[转] laravel-mysql 读写分离

原文地址 : laravel-mysql 读写分离

使用【如果不想了解源代码,直接看 3 种使用方式就好】

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
'mysql' => [
'write' => [
'host' => '127.0.0.1',
'port' => 3306
],
'read' => [
[
'host' => '127.0.0.1',
'port' => 3307
],
],
'driver' => 'mysql',
// 'host' => env('DB_HOST', '127.0.0.1'),
// 'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
// 'charset' => 'utf8mb4',
// 'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
// 'sticky' => true, // laravel 5.5 新增
],

使用写库读数据的三种方式

  • 方法 1:
1
$user = DB::selectFromWriteConnection('select * from users where id=42111');
  • 方法 2:
1
User::onWriteConnection()->find($id);
  • 方法 3:

通过配置  'sticky' => true,

一 配置过程

config/database.php 里面配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
'mysql' => [
'write' => [
'host' => '192.168.1.180',
],
'read' => [
['host' => '192.168.1.182'],
['host' => '192.168.1.179'],
],
'sticky' => true, // laravel 5.5 新增
'driver' => 'mysql',
'port' => env('DB_PORT', '3306'),
'unix_socket' => env('DB_SOCKET', ''),
'engine' => null,
'database' => 'database',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
]

加强版,支持多主多从,支持独立用户名和密码,配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
'mysql' => [
'write' => [
[
'host' => '192.168.1.180',
'username' => '',
'password' => '',
],
],
'read' => [
[
'host' => '192.168.1.182',
'username' => '',
'password' => '',
],
[
'host' => '192.168.1.179',
'username' => '',
'password' => '',
],
],
'driver' => 'mysql',
'database' => 'database',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
]

设置完毕之后,Laravel5 默认将  select  的语句让  read  指定的数据库执行,insert/update/delete  则交给  write  指定的数据库,达到读写分离的作用。

这些设置对原始查询  raw queries,查询生成器  query builder,以及  Eloquent ORM  都生效。

官网解释如下:

Sometimes you may wish to use one database connection for SELECT statements, and another for INSERT, UPDATE, and DELETE statements. Laravel makes this a breeze, and the proper connections will always be used whether you are using raw queries, the query builder, or the Eloquent ORM

验证

开启 MySQL 的  general-log ,通过  tail -f  的方式监控 log 变化来确定配置是否生效

注意点 1:

sticky  是一个 可选的 选项,它可用于立即读取在当前请求周期内已写入数据库的记录。

如果  sticky  选项被启用,并且在当前的请求周期内在数据库执行过「写入」操作,那么任何「读取」的操作都将使用「写入」连接。这可以确保在请求周期内写入的任何数据可以在同一请求期间立即从数据库读回。这个选项的作用取决于应用程序的需求。【sticky 选项是一个可选的配置值,可用于在当前请求生命周期内允许立即读取写入数据库的记录。如果 sticky 选项被启用并且一个”写”操作在当前生命周期内发生,则后续所有”读”操作都会使用这个”写”连接(前提是同一个请求生命周期内),这样就可以确保同一个请求生命周期内写入的数据都可以立即被读取到,从而避免主从延迟导致的数据不一致,是否启用这一功能取决于你。】

当然,这只是一个针对分布式数据库系统中主从数据同步延迟的一个非常初级的解决方案,访问量不高的中小网站可以这么做,大流量高并发网站肯定不能这么干,主从读写分离本来就是为了解决单点性能问题,这样其实是把问题又引回去了,造成所有读写都集中到写数据库,对于高并发频繁写的场景下,后果可能是不堪设想的,但是话说回来,对于并发量不那么高,写操作不那么频繁的中小型站点来说,sticky 这种方式不失为一个初级的解决方案。

注意点 2:

注:目前读写分离仅支持单个写连接。

二 实现原理

Laravel5 读写分离主要有两个过程:

第一步,根据  database.php  配置,创建写库和读库的链接  connection

第二步,调用  select  时先判断使用读库还是写库,而  insert/update/delete  统一使用写库

三 源码分析:根据 database.php 配置,创建写库和读库的链接 connection

主要文件:/vendor/laravel/framework/src/Illuminate/Database/Connectors/ConnectionFactory.php  来看看几个重要的函数:

  • 判断  database.php  是否配置了读写分离数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Establish a PDO connection based on the configuration.
*
* @param array $config
* @param string $name
* @return \Illuminate\Database\Connection
*/
public function make(array $config, $name = null)
{
$config = $this->parseConfig($config, $name);

// 如果配置了读写分离,则同时创建读库和写库的链接【因为写库也可以读】
if (isset($config['read'])) {
return $this->createReadWriteConnection($config);
}

// 如果没有配置,默认创建单个数据库链接
return $this->createSingleConnection($config);
}
  • 创建读库和写库的链接
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Create a single database connection instance.
*
* @param array $config
* @return \Illuminate\Database\Connection
*/
protected function createReadWriteConnection(array $config)
{
// 获取写库的配置信息,并创建链接
$connection = $this->createSingleConnection($this->getWriteConfig($config));
// 创建读库的链接
return $connection->setReadPdo($this->createReadPdo($config));
}
  • 多个读库会选择哪个呢

旧版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Get the read configuration for a read / write connection.
*
* @param array $config
* @return array
*/
protected function getReadConfig(array $config)
{
$readConfig = $this->getReadWriteConfig($config, 'read');

// 如果数组即多个读库,那么通过随机函数array_rand()挑一个,默认取第一个
if (isset($readConfig['host']) && is_array($readConfig['host'])) {
$readConfig['host'] = count($readConfig['host']) > 1
? $readConfig['host'][array_rand($readConfig['host'])]
: $readConfig['host'][0];
}
return $this->mergeReadWriteConfig($config, $readConfig);
}

新版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* Get the read configuration for a read / write connection.
*
* @param array $config
* @return array
*/
protected function getReadConfig(array $config)
{
return $this->mergeReadWriteConfig(
$config, $this->getReadWriteConfig($config, 'read')
);
}

/**
* Merge a configuration for a read / write connection.
*
* @param array $config
* @param array $merge
* @return array
*/
protected function mergeReadWriteConfig(array $config, array $merge)
{
return Arr::except(array_merge($config, $merge), ['read', 'write']);
}

/**
* Get a read / write level configuration.
*
* @param array $config
* @param string $type
* @return array
*/
protected function getReadWriteConfig(array $config, $type)
{
return isset($config[$type][0])
? Arr::random($config[$type])
: $config[$type];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
class Arr
{
...

/**
* Get all of the given array except for a specified array of keys.
*
* @param array $array
* @param array|string $keys
* @return array
*/
public static function except($array, $keys)
{
static::forget($array, $keys);

return $array;
}

...

/**
* Remove one or many array items from a given array using "dot" notation.
*
* @param array $array
* @param array|string $keys
* @return void
*/
public static function forget(&$array, $keys)
{
$original = &$array;

$keys = (array) $keys;

if (count($keys) === 0) {
return;
}

foreach ($keys as $key) {
// if the exact key exists in the top-level, remove it
if (static::exists($array, $key)) {
unset($array[$key]);

continue;
}

$parts = explode('.', $key);

// clean up before each pass
$array = &$original;

while (count($parts) > 1) {
$part = array_shift($parts);

if (isset($array[$part]) && is_array($array[$part])) {
$array = &$array[$part];
} else {
continue 2;
}
}

unset($array[array_shift($parts)]);
}
}

...

/**
* Get one or a specified number of random values from an array.
*
* @param array $array
* @param int|null $number
* @return mixed
*
* @throws \InvalidArgumentException
*/
public static function random($array, $number = null)
{
$requested = is_null($number) ? 1 : $number;

$count = count($array);

if ($requested > $count) {
throw new InvalidArgumentException(
"You requested {$requested} items, but there are only {$count} items available."
);
}

if (is_null($number)) {
return $array[array_rand($array)];
}

if ((int) $number === 0) {
return [];
}

$keys = array_rand($array, $number);

$results = [];

foreach ((array) $keys as $key) {
$results[] = $array[$key];
}

return $results;
}
}
  • 写库也是随机选择的

旧版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Get a read / write level configuration.
*
* @param array $config
* @param string $type
* @return array
*/
protected function getReadWriteConfig(array $config, $type)
{

// 如果多个,那么通过随机函数array_rand()挑一个
if (isset($config[$type][0])) {
return $config[$type][array_rand($config[$type])];
}
return $config[$type];
}

新版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Get a read / write level configuration.
*
* @param array $config
* @param string $type
* @return array
*/
protected function getReadWriteConfig(array $config, $type)
{
return isset($config[$type][0])
? Arr::random($config[$type])
: $config[$type];
}

总结

  • 可以设置多个读库和多个写库,或者不同组合,比如一个写库两个读库

  • 每次只创建一个读库链接和一个写库链接,从多个库中随机选择一个

四 源码分析:调用 select 时先判断使用读库还是写库,而 insert/update/delete 统一使用写库

主要文件:/vendor/laravel/framework/src/Illuminate/Database/Connection.php  看看几个重要的函数

  • select  函数根据第三个输入参数判断使用读库还是写库(true 使用读库,false 使用写库;默认使用读库)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* Run a select statement against the database.
*
* @param string $query
* @param array $bindings
* @param bool $useReadPdo
* @return array
*/
public function select($query, $bindings = [], $useReadPdo = true)
{
return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) {
if ($this->pretending()) {
return [];
}

// 根据$useReadPdo参数,判断使用读库还是写库;
// true使用读库,false使用写库;默认使用读库
// For select statements, we'll simply execute the query and return an array
// of the database result set. Each element in the array will be a single
// row from the database table, and will either be an array or objects.
$statement = $this->prepared($this->getPdoForSelect($useReadPdo)
->prepare($query));

$this->bindValues($statement, $this->prepareBindings($bindings));

$statement->execute();

return $statement->fetchAll();
});
}

/**
* Get the PDO connection to use for a select query.
*
* @param bool $useReadPdo
* @return \PDO
*/
protected function getPdoForSelect($useReadPdo = true)
{
// 根据$useReadPdo参数,选择PDO即判断使用读库还是写库;
// true使用读库getReadPdo,false使用写库getPdo;
return $useReadPdo ? $this->getReadPdo() : $this->getPdo();
}
  • insert/update/delete  统一使用写库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* Run an insert statement against the database.
*
* @param string $query
* @param array $bindings
* @return bool
*/
public function insert($query, $bindings = [])
{
return $this->statement($query, $bindings);
}

/**
* Run an update statement against the database.
*
* @param string $query
* @param array $bindings
* @return int
*/
public function update($query, $bindings = [])
{
return $this->affectingStatement($query, $bindings);
}

/**
* Run a delete statement against the database.
*
* @param string $query
* @param array $bindings
* @return int
*/
public function delete($query, $bindings = [])
{
return $this->affectingStatement($query, $bindings);
}

/**
* Execute an SQL statement and return the boolean result.
*
* @param string $query
* @param array $bindings
* @return bool
*/
public function statement($query, $bindings = [])
{
return $this->run($query, $bindings, function ($query, $bindings) {
if ($this->pretending()) {
return true;
}

// 直接调用写库
$statement = $this->getPdo()->prepare($query);

$this->bindValues($statement, $this->prepareBindings($bindings));

$this->recordsHaveBeenModified();

return $statement->execute();
});
}

/**
* Run an SQL statement and get the number of rows affected.
*
* @param string $query
* @param array $bindings
* @return int
*/
public function affectingStatement($query, $bindings = [])
{
return $this->run($query, $bindings, function ($query, $bindings) {
if ($this->pretending()) {
return 0;
}

// 直接调用写库
// For update or delete statements, we want to get the number of rows affected
// by the statement and return that back to the developer. We'll first need
// to execute the statement and then we'll use PDO to fetch the affected.
$statement = $this->getPdo()->prepare($query);

$this->bindValues($statement, $this->prepareBindings($bindings));

$statement->execute();

$this->recordsHaveBeenModified(
($count = $statement->rowCount()) > 0
);

return $count;
});
}

总结:

  • getReadPdo()  获得读库链接,getPdo()  获得写库链接;

  • select()  函数根据第三个参数判断使用读库还是写库;

五 强制使用写库

有时候,我们需要读写实时一致,写完数据库后,想马上读出来,那么读写都指定一个数据库即可。 虽然 Laravel5 配置了读写分离,但也提供了另外的方法强制读写使用同一个数据库。

实现原理:上面  $this->select()  时指定使用写库的链接,即第三个参数  useReadPdo  设置为  false  即可。

有几个方法可实现:

  • 调用方法 1: DB::table('users')->selectFromWriteConnection('*')->where('id', $id)->first();

$user = DB::selectFromWriteConnection('select * from users where id=42111');

源码解释:通过  selectFromWriteConnection()  函数 主要文件:

/vendor/laravel/framework/src/Illuminate/Database/Connection.php

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Run a select statement against the database.
*
* @param string $query
* @param array $bindings
* @return array
*/
public function selectFromWriteConnection($query, $bindings = [])
{
// 上面有解释$this->select()函数的第三个参数useReadPdod的意义
// 第三个参数是 false,所以 select 时会使用写库,而不是读库
return $this->select($query, $bindings, false);
}
  • 调用方法 2: User::onWriteConnection()->find($id);

源码解释:通过  onWriteConnection()  函数 主要文件:

/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php

1
2
3
4
5
6
7
8
9
10
11
/**
* Begin querying the model on the write connection.
*
* @return \Illuminate\Database\Query\Builder
*/
public static function onWriteConnection()
{
$instance = new static;
// query builder 指定使用写库
return $instance->newQuery()->useWritePdo();
}

再看看  query builder  如何指定使用写库 主要文件:

/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Use the write pdo for query.
*
* @return $this
*/
public function useWritePdo()
{
// 指定使用写库,useWritePdo 为true
$this->useWritePdo = true;

return $this;
}

/**
* Run the query as a "select" statement against the connection.
*
* @return array
*/
protected function runSelect()
{
// 执行select时,useWritePdo原值为true,这里取反,被改成false;
// 即$this->select()函数第三个参数为false,所以使用写库;
return $this->connection->select($this->toSql(), $this->getBindings(), ! $this->useWritePdo);
}

开启日志验证

使用 mysql general log 来验证数据库读写分离

主数据库开启 general log

1
2
3
4
5
6
7
8
9
10
11
mysql> show global variables like '%general%';
+------------------+------------------------------------------------------+
| Variable_name | Value |
+------------------+------------------------------------------------------+
| general_log | OFF |
| general_log_file | D:\soft\phpstudy\PHPTutorial\MySQL\data\admin-PC.log |
+------------------+------------------------------------------------------+
2 rows in set (0.00 sec)

mysql> set global general_log = on;
Query OK, 0 rows affected (0.05 sec)

从数据库开启 general log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> show global variables like '%general%';
+------------------+-----------------------------------------------------------+

| Variable_name | Value |

+------------------+-----------------------------------------------------------+

| general_log | OFF |

| general_log_file | D:\soft\mysql5.7.24\mysql-5.7.24-winx64\data\admin-PC.log |

+------------------+-----------------------------------------------------------+

2 rows in set, 1 warning (0.00 sec)

mysql> set global general_log = on;
Query OK, 0 rows affected (0.04 sec)

关闭日志

1
2
mysql> set global general_log = off;
Query OK, 0 rows affected (0.01 sec)
1
2
3
4
// 指定log路径
// set global general_log_file='/tmp/general.lg'; #设置路径
// 也可以将日志记录在表中
// set global log_output='table' // 可以在mysql数据库下查找 general_log表

问题

问题 1

写入了数据,但查询时却报 No query result ,而且只是偶然性出现,没啥规律。

解决方案

初以为是 prettus/l5-repository 包的缓存引起的,但关掉它的缓存功能后问题依旧。后来好一阵折腾,直到再一次仔细翻看文档, 才发现 Laravel5.5 数据库读写分离配置的部分额外提到了一个 sticky 项,文档里这部分原文如下:

The sticky Option

The sticky option is an optional value that can be used to allow the immediate reading of records that have been written to the database during the current request cycle. If the sticky option is enabled and a “write” operation has been performed against the database during the current request cycle, any further “read” operations will use the “write” connection. This ensures that any data written during the request cycle can be immediately read back from the database during that same request. It is up to you to decide if this is the desired behavior for your application.

在没有启用  sticky  的时候,使用  write  连接写入数据后立即读取,读取时使用的是  read  连接,这样就有可能出问题。将  sticky  设置为  true  后,在与这个写入操作相同的请求周期内的后续读取操作,仍然使用原来的  write  连接。

或者  强制使用写库

参考资料:

Laravel 5 配置读写分离和源码分析

Laravel 5 配置数据库主从读写分离和源码分析

Forms & HTML 组件 (laravelcollective/html)

原文地址: Laravel Collective Forms & HTML

安装

首先通过 composer 来安装这个 包, 编辑项目的 composer.json 文件. 在 require 部分 加入 laravelcollective/html :

1
2
3
"require": {
"laravelcollective/html": "5.1.*"
}

接下来从命令行更新 composer  :

1
composer update -vvv

接下来添加 provider 到 config/app.phpproviders 数组:

1
2
3
4
5
'providers' => [
// ...
Collective\Html\HtmlServiceProvider::class,
// ...
],

最后 添加两个类链接到 config/app.phpaliases 数组:

1
2
3
4
5
6
'aliases' => [
// ...
'Form' => Collective\Html\FormFacade::class,
'Html' => Collective\Html\HtmlFacade::class,
// ...
],

##创建表单

打开表单

1
2
3
{!! Form::open(array('url' => 'foo/bar')) !!}
//
{!! Form::close() !!}

默认是 POST 方法, 你可以随意指定其他接收方法

1
echo Form::open(array('url' => 'foo/bar', 'method' => 'put'))

Note: HTML 表单仅仅支持 POSTGET方法, PUTDELETE 方法将会使用一个隐藏域_method 添加到 form 表单中来欺骗实现

你可以使用指定的控制器@动作 或者命名的路由来创建表单

1
2
3
echo Form::open(array('route' => 'route.name'))

echo Form::open(array('action' => 'Controller@method'))

同样也可以向路由中传入参数.

1
2
3
echo Form::open(array('route' => array('route.name', $user->id)))

echo Form::open(array('action' => array('Controller@method', $user->id)))

如果你的表单需要支持文件上传, 在数组中添加 一个 files 配置项.

1
echo Form::open(array('url' => 'foo/bar', 'files' => true))

CSRF 保护

向表单中添加 CSRF Token

Laravel 提供了一个简单的方法来防止你的应用遭受跨站攻击. 首先会在你的 session 中生成一个随机的 token, 如果你使用 Form::open 方法并且提交方法是 POST, PUT或者是 DELETE,  CSRF token 将会自动的添加到你的 form 表单的隐藏域中. 换种方法 如果你像自己生成  CSRF token 字段, 你可以使用 token 方法.

1
echo Form::token();

给路由添加  CSRF 过滤器

1
2
3
4
Route::post('profile', array('before' => 'csrf', function()
{
//
}));

表单模型绑定

给表单绑定模型

通常, 你需要想表单中填入来自数据库模型的数据. 想这样做你可以使用 Form::model 方法.

1
echo Form::model($user, array('route' => array('user.update', $user->id)))

现在当你自动生成一个表单元素, 例如文本输入框. Model 的值将会自动匹配并且填写到相关的表单字段中.例如. 一个文本输入框的 name 是 email这个字段将会用 用户 Modelemail 属性来填充并且设置. 当然, 还有其他用法. 如果一个字段在 session 闪存数据中 也存在这个名字, 这个将会覆盖模型中的这个字段值. 优先级是这个样子的:

  1. Session Flash Data (Old Input) [session 闪存 / 老的输入数据 ]
  2. Explicitly Passed Value [输入值]
  3. Model Attribute Data [模型属性值]

这个可以让你快速的使用模型数据来创建表单, 也能轻松的在服务器校验错误之后重新发布表单.

Note: 使用 Form::model方法的时候一定要使用 Form::close来关闭表单!

标签

生成标签元素

1
echo Form::label('email', 'E-Mail Address');

指定额外的 html 属性

1
echo Form::label('email', 'E-Mail Address', array('class' => 'awesome'));

Note: 在创建了一个标签之后, 如果有创建的表单元素的 name 值和 label 的 name 值相符的话, 将会自动在 表单元素 中自动匹配增加 id 属性. id 的值就是 label 的 name 值.

文本框, 文本域, 密码 & 隐藏域

创建文本框

1
echo Form::text('username');

指定默认值

1
echo Form::text('email', 'example@gmail.com');

Note:  hiddentextarea 方法的参数和 text 相同.

生成密码输入框

1
echo Form::password('password', array('class' => 'awesome'));

生成其他输入框

1
2
echo Form::email($name, $value = null, $attributes = array());
echo Form::file($name, $attributes = array());

多选和单选

生成单选和多选

1
2
echo Form::checkbox('name', 'value');
echo Form::radio('name', 'value');

生成带有选中状态的表单元素

1
2
echo Form::checkbox('name', 'value', true);
echo Form::radio('name', 'value', true);

数字

生成数字输入框

1
echo Form::number('name', 'value');

日期

生成日期输入框

1
echo Form::date('name', \Carbon\Carbon::now());

文件选择器

生成文件选择器

1
echo Form::file('image');

Note: 表单中必须设置 files 参数的值为 true

下拉列表

生成下拉列表

1
echo Form::select('size', array('L' => 'Large', 'S' => 'Small'));

生成有默认值的下拉列表

1
echo Form::select('size', array('L' => 'Large', 'S' => 'Small'), 'S');

生成空占位符的 下拉列表

这回创建一个没有任何值的 <option> 元素作为下拉列表的第一个选择值.

1
echo Form::select('size', array('L' => 'Large', 'S' => 'Small'), null, ['placeholder' => 'Pick a size...']);

生成分组的列表

1
2
3
4
echo Form::select('animal', array(
'Cats' => array('leopard' => 'Leopard'),
'Dogs' => array('spaniel' => 'Spaniel'),
));

生成范围选择值的下拉列表

1
echo Form::selectRange('number', 10, 20);

生成有月份名称的选择值

1
echo Form::selectMonth('month');

按钮

生成提交按钮

1
echo Form::submit('Click Me!');

Note: 想创建一个按钮元素? 试用 button 方法. 他和 submit 方法有相同的参数.

自定义表单元素

注册一个新的表单元素

用来很方便的来自定义一个表单元素的方法叫做 macros . 合理是怎样使用它. 首先简单的使用名称和闭包函数来注册一个 :

1
2
3
4
Form::macro('myField', function()
{
return '<input type="awesome">';
});

现在你可以使用自定义的名字来调用这个 macro

调用自定义的 Form Macro

1
echo Form::myField();

生成 URL

根据给定的 URL 生成 html 链接

1
echo link_to('foo/bar', $title = null, $attributes = array(), $secure = null);

生成一个链接到指定资源的 html

1
echo link_to_asset('foo/bar.zip', $title = null, $attributes = array(), $secure = null);

生成一个根据给定路由的 html 链接

1
echo link_to_route('route.name', $title = null, $parameters = array(), $attributes = array());

根据指定的控制器/方法来生成 html 链接

1
echo link_to_action('HomeController@getIndex', $title = null, $parameters = array(), $attributes = array());

[译] Laravel-mix 4.0 中文文档

原文地址: Laravel Mix Docs

概览

升级

升级到 v4.0

1
2
npm remove laravel-mix
npm install laravel-mix@^4.0.0 --save-dev

升级后,如果遇到任何相关 vue-template-compiler 的问题,这与你安装的 vue 版本号 vue-template-compiler 版本号必须相同。更新其中一个或两个以解决此问题。

新特性

  • 编译速度更快。
  • 安装 npm 速度更快。
  • 升级到 webpack 4。
  • 升级到 vue-loader 15。
  • 升级到 Babel 7。
  • 自动第三方包获取。如果 mix.extract() 不使用参数调用,node_modules/ 将自动提取所有依赖项(您引入的任何包)。!
  • 可以提供 CSS 压缩(via cssnano)选项。
  • PostCSS 插件可以分别传递给 mix.sass/less/stylus() 。这意味着mix.sass() 如果需要,您可以为每个插件提供唯一的 PostCSS 插件。
  • JS optimizing/minification 从 Uglify 切换 Terser。
  • 从 node-sass 切换到 Dart Sass。虽然这会带来较小的编译时间成本,但 npm 安装的好处是更快,更可靠。
  • 改进的 Babel 配置合并策略。你现在可以通过 .babelrc 在项目根目录中创建文件来覆盖或调整通过 Mix 提供的任何默认 Babel 插件和预设。

修复

  • 由于升级到 webpack 4,所有 npm 警报都已修复。

备注

  • 如果你的项目大量使用 JavaScript 动态导入,你可能需要等到到明年初发布的 webpack 5。有与此相关的已知编译问题,在此之前我们无法解决。一旦 webpack 5 推出,Mix 将在不久后更新。如果您不熟悉动态导入,那么这很可能不会影响您的项目
  • Sass 支持现在是按需依赖。在混合之前的版本中,node-sass并且sass-loader依赖被列入开箱即用,不管你的项目是否需要 Sass 编译。为了帮助缩短安装时间,当且仅当您的项目指定了 Sass 编译时,才会按需安装这两个依赖项mix.sass()。第一次运行时npm run dev,将安装依赖项并将其保存到 dev-dependencies 列表中。

导入 ES 模块

作为 vue-loader 15 更新的一部分,如果您的代码使用 CommonJS 语法导入 EcmaScript 模块,则需要追加 .default,如下所示:

1
2
3
4
5
Vue.component(
'example-component',
- require('./components/ExampleComponent.vue')
+ require('./components/ExampleComponent.vue').default
);

建议切换到 EcmaScript 导入:

1
2
3
import ExampleComponent from './components/ExampleComponent.vue';

Vue.component('example-component', ExampleComponent);

Babel 7 支持

官方 Babel 插件的命名约定已经改变。
它们现在位于**@babel**命名空间下。

更新您的package.json并更改所有出现的"babel-plugin-[name]"

1
2
- "babel-plugin-[name]": "6.x"
+ "@babel/plugin-[name]": "7.x"

如果你在项目中创建了一个 .babelrc 文件,请更新所有插件名称引用:

1
2
- "plugins": ["babel-plugin-transform-object-rest-spread"]
+ "plugins": ["@babel/plugin-proposal-object-rest-spread"]

从 Node Sass 切换到 Dart Sass

如果我们从 node-sass 切换到 dart-sass,虽然在很大程度上是相同的,你可能会注意到在编译时改变或警告。您可以逐个解决这些问题,也可以手动切换回 node-sass,如下所示:

1
npm install node-sass
1
2
3
mix.sass('resources/sass/app.sass', 'public/css', {
implementation: require('node-sass')
});

删除了 fastSass()和 standaloneSass()

mix.fastSass()mix.standaloneSass()(别名)已被完全删除。
为了提高那些只需要编译 CSS 的人的性能,这个命令提供了与核心 webpack 构建分开的 Sass 编译。
然而,它给新手带来的更多的是困惑而不是有帮助。
mix.fastSass() 到迁移 mix.sass()

1
2
3
- mix.fastSass()
- mix.standaloneSass()
+ mix.sass()

删除已弃用的.mix属性

已弃用的 .mix 属性现已删除。
如果您有 require('laravel-mix').mix webpack.mix.js 文件,请将其更改为 require('laravel-mix')

1
2
- require('laravel-mix').mix
+ require('laravel-mix')

从 Uglify 切换到 Terser

由于强制从 Uglify 切换到 Terser,
如果您的项目覆盖了默认配置
Config.uglify = {},则需要切换到Config.terser = {}
选项 API在很大程度上是相同的。

webpack.mix.js:

1
2
3
4
5
6
7
8
9
mix.options({
- uglify: {
- uglifyOptions: {
+ terser: {
+ terserOptions: {
warnings: true
}
}
});

Vue 组件 Sass 预处理

如果您的项目不包含mix.sass()调用(自动下载所有必需的依赖项),但lang="sass"在 Vue 组件中指定,则可能需要安装 node-sass 或 sass。

因为 Mix 不知道你在 Vue 组件中指定了哪些预处理器,所以
需要手动将它们引入。

你可以解决这个问题:

1
npm install node-sass sass-loader

要么

1
npm install sass sass-loader

请注意,Less 和 Stylus 也是如此。

基本示例

larave-mix 是位于 webpack 顶层的一个简洁的配置层,在 80% 的情况下使用 laravel mix 会使操作变的非常简单。尽管 webpack 非常的强大,但大部分人都认为 webpack 的学习成本非常高。但是如果你不必用再担心这些了呢?

看一下基本的 webpack.mix.js 文件,让我们想象一下我们现在只需要编译 javascript(ES6)和 sass 文件:

1
2
3
4
let mix = require('laravel-mix');

mix.sass('src/app.sass', 'dist')
.js('src/app.js', 'dist');

怎么样,简单吗?

  1. 编译 sass 文件, ./src/app.sass./dist/app.css
       2. 打包在 ./src/app.js 的所有 js(包括任何依赖)到 ./dist/app.js

使用这个配置文件,可以在命令行触发 webpack 指令:node_modules/bin/webpack

在开发模式下,并不需要压缩输出文件,如果在执行 webpack 的时候加上环境变量:export NODE_ENV=production && webpack,文件会自动压缩

less ?

但是如果你更喜欢使用 Less 而不是 Sass 呢?没问题,只要把 mix.sass() 换成 mix.less()就 OK 了。

使用 laravel-mix,你会使发现大部分 webpack 任务会变得更又把握

安装

尽管 laravel-mix 对于 laravel 来做的优化工具,但也能被用于其他任何应用。

laravel 项目

laravel 已经包含了你所需要的一切,简易步骤:

  1. 安装 laravel

  2. 运行 npm install

  3. 查看 webpack.mix.js 文件 ,就可以开始使用了.

你可以在命令行运行 npm run watch 来监视你的前段资源改变,然后重新编译。

在项目根目录下并没有 webpack.config.js 配置文件,laravel 默认指向根目录下的配置文件。如果你需要自己配置它,你可以把它拷贝到根目录下,同时修改 package.json 里的 npm 脚本: cp node_modules/laravel-mix/setup/webpack.config.js ./.

独立项目

首先使用 npm 或者 yarn 安装 laravel-mix,然后把示例配置文件复制到项目根目录下

1
2
3
4
mkdir my-app && cd my-app
npm init -y
npm install laravel-mix --save-dev
cp -r node_modules/laravel-mix/setup/webpack.mix.js ./

现在你会有如下的目录结构

1
2
3
node_modules/
package.json
webpack.mix.js

webpack.mix.js 是你在 webpack 上层的配置文件,大部分时间你需要修改的是这个文件

首先看下 webpack.mix.js 文件

1
2
3
4
5
let mix = require('laravel-mix');

mix.js('src/app.js', 'dist')
.sass('src/app.scss', 'dist')
.setPublicPath('dist');

注意源文件的路径,然后创建匹配的目录结构(你也可以改成你喜欢的结构)。现在都准备好了,在命令行运行 node_modules/.bin/webpack 编译所有文件,然后你将会看到:

  • dist/app.css
  • dist/app.js
  • dist/mix-manifest.json(你的 asset 输出文件,稍后讨论)

干得漂亮!现在可以干活了。

NPM Scripts

把下面的 npm 脚本添加到你的 package.json 文件中可以加速你的工作操作.,laravel 安装的时候已经包含了这个东西了

1
2
3
4
5
6
7
8
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"hot": "cross-env NODE_ENV=development webpack-dev-server --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
}

laravel 工作流程

我们先回顾一下通用的工作流程以便你能在自己的项目上采用

1 . 安装 laravel

1
laravel new my-app

2 . 安装 Node 依赖

1
npm install

**3 . 配置 webpack.mix.js **

这个文件所有前端资源编译的入口

1
2
3
4
let mix = require('laravel-mix');

mix.js('resources/assets/js/app.js', 'public/js');
mix.sass('resources/assets/sass/app.scss', 'public/css');

默认会启用 JavaScript ES2017 + 模块绑定,就行 sass 编译一样。

4 . 编译

用如下指令编译

1
node_modules/.bin/webpack

也可以使用 package.json 里的 npm 脚本:

1
npm run dev

然后会看到编译好的文件:

  • ./public/js/app.js
  • ./public/css/app.css

监视前端资源更改:

1
npm run watch

Laravel 附带一个ExampleComponent.vue文件,你可以在 components 文件夹中找到:

1
./resources/js/components/ExampleComponent.vue

修改一下,然后等待操作系统通知,这表示编译已完成!

mix.browserSync('myapp.test') 当 Laravel 应用程序中的任何相关文件发生更改时,您还可以使用自动重新加载浏览器。

5 . 更新视图

laravel 自带一个欢迎页面,我们可以用这个来做示例,修改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Laravel</title>

<link rel="stylesheet" href="{{ mix('css/app.css') }}" />
</head>
<body>
<div id="app">
<example-component></example-component>
</div>

<script src="{{ mix('js/app.js') }}"></script>
</body>
</html>

刷新页面,干得漂亮!

常见问题

laravel-mix 必须在 laravel 下使用吗?

不,在 laravel 下使用使最好的,但也可以用在别的任何项目

我的代码没有压缩

只有在 node 环境变量为生产环境时才会被压缩,这样会加速编译过程,但在开发过程中是不必要的,下面是在生成环境下运行 webpack 的示例

1
export NODE_ENV=production && webpack --progress --hide-modules

强烈推荐你把下面的 npm 脚本添加到你的 package.json 文件中,注意 laravel 已经包括了这些了

1
2
3
4
5
6
"scripts": {
"dev": "NODE_ENV=development webpack --progress --hide-modules",
"watch": "NODE_ENV=development webpack --watch --progress --hide-modules",
"hot": "NODE_ENV=development webpack-dev-server --inline --hot",
"production": "NODE_ENV=production webpack --progress --hide-modules"
},

我使用的是 VM,webpack 不能检测到我的文件变化

如果你在 VM 下执行 npm run dev,你会发现 webpack 并不能监视到你的文件改变。如果这样的话,有两种方式来解决这个

  1. 配置 webpack 检测文件系统的变化, 注意:检测文件系统是资源密集型操作并且很耗费电池的使用时长.
  2. 转发文件通过使用类似于 vagrant-fsnotify 之类的东西将通知发送给 VM。注意,这是一个 只有 Vagrant 才有的插件。

检测 VM 文件系统变化, 修改一下你的 npm 脚本,使用 --watch-poll--watch 标签,像这样:

1
2
3
"scripts": {
"watch": "NODE_ENV=development webpack --watch --watch-poll",
}

推送文件改动到 VM, 在主机安装 vagrant-fsnotify

1
vagrant plugin install vagrant-fsnotify

现在你可以配置 vargrant 来使用这个插件, 在 Homestead 中, 在你的 Homestead.yaml 文件类似于这样

1
2
3
4
5
6
7
8
folders:
- map: /Users/jeffrey/Code/laravel
to: /home/vagrant/Code/laravel
options:
fsnotify: true
exclude:
- node_modules
- vendor

一旦你的 vagrant 机器启动, 只需要在主机上运行 vagrant fsnotify 把文件的改动推送到 vm 上, 然后在 vm 内部运行 npm run watch 就能够检测到文件的改动了.

如果你还是有问题,去这儿溜达溜达吧

为什么在我的 css 文件里显示图片在 node_modules 里找不到

你可能用的是相对路径,但是在你的 resources/assets/sass/app.css 里并不存在:

1
2
3
body {
background: url('../img/example.jpg');
}

当引用相对路径的时候,会根据当前文件的路径来搜索,同样的,webpack 会首先搜索 `resources/assets/img/example.jpg ,如果找不到,会继续搜索文件位置,包括 node_modules,如果还找不到,就报错:

1
2
3
ERROR  Failed to compile with 1 errors

This dependency was not found in node_modules:

有两个解决办法:

1 . 让 resources/assets/img/example.jpg 存在这个文件.
2 . 编译 css 的时候添加下面的选项,禁用 css 的 url 处理:

1
2
3
mix.sass("resources/assets/sass/app.scss", "public/css").options({
processCssUrls: false,
});

他对老项目特别有用,因为你的文件夹结构已经完全创建好了。

我不想把 mix-manifest.json 文件放在项目根目录下

如果你没有使用 laravel,你的 mix-manifest.json 文件会被放到项目根目录下,如果你不喜欢的话,可以调用 mix.setPublicPath('dist/'),然后 manifest 文件就会被放到 dist 目录下。

怎样使用 webpack 自动加载模块

webpack 使用 ProvidePlugin 插件加载一些需要的模块,常用的一个例子就是加载 jQuery:

1
2
3
4
5
6
7
8
9
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
});

// in a module
$('#item'); // <= just works
jQuery('#item'); // <= just works
// $ is automatically set to the exports of module "jquery"

当 laravel-mix 自动加载模块的时候(像上面说的那样),你如果想禁用(传一个空对象)或者用你自己的模块覆盖它,可以调用 mix.autoload() 方法:

1
2
3
4
mix.autoload({
jquery: ['$', 'window.jQuery', 'jQuery'], // more than one
moment: 'moment' // only one
});

为什么我看到一个 “Vue packages version mismatch”错误

如果, 更新你的依赖, 你有以下编译失败的信息

1
2
3
4
5
6
Module build failed: Error:

Vue packages version mismatch:

* vue@2.5.13
* vue-template-compiler@2.5.15

这意味着你的 vuevue-template-compiler 依赖不同步, 每一个 Vue 的更新, 版本号必须是相同的. 更新来修复这个错误

1
2
3
4
5
npm update vue

// or

npm install vue@2.5.15

排障

我在更新/安装 mix 时候出现错误

不幸的是,你的依赖项可能没有正确安装的原因有无数个。一个常见的根本原因是安装了老版本的 Node(node -v) 和 npm (npm -v)。第一步,访问 http://nodejs.org 并更新它们。

否则,它通常与需要删除的错误锁文件有关。让这一系列命令尝试从头开始安装一切

1
2
3
4
rm -rf node_modules
rm package-lock.json yarn.lock
npm cache clear --force
npm install

为什么 webpack 不能找到我的 app.js 条目文件?

如果你遇到这样的失败信息……

1
2
3
These dependencies were not found:

* /Users/you/Sites/folder/resources/assets/js/app.js

… 你可能使用 npm 5.2 (npm -v) 这个版本。这个版本引入了一个导致安装错误的错误。该问题已被在 npm 5.3 修复。请升级,然后重新安装

1
2
3
4
rm -rf node_modules
rm package-lock.json yarn.lock
npm cache clear --force
npm install

API

Javascript

1
mix.js(src|[src], output)

简单的一行代码,larave mix 可以执行很多重要的操作

  • ES2017+ 模块编译
  • 创建并且编译 vue 组件(通过 vue-loader)
  • 模块热替换(HMR)
  • Tree-shaking 打包技术,webpack2 里新增的(移除无用的库)
  • 提取和拆分 vendor 库(通过mix.extract()方法), 使长期缓存变的容易
  • 自动版本化(文件哈希),通过 mix.version()

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let mix = require('laravel-mix');

// 1. A single src and output path.
mix.js('src/app.js', 'dist/app.js');


// 2. For additional src files that should be
// bundled together:
mix.js([
'src/app.js',
'src/another.js'
], 'dist/app.js');


// 3. For multiple entry/output points:
mix.js('src/app.js', 'dist/')
.js('src/forum.js', 'dist/');

laravel 示例

考虑到典型的 laravel 默认安装的时候会把入口定位在 ./resources/assets/js/app.js,所以我们先准备一个 webpack.mix.jsapp.js 编译到 ./public/js/app.js

1
2
3
let mix = require('laravel-mix');

mix.js('resources/assets/js/app.js', 'public/js');

现在上面所有的项你都可以用了,只需要调用一个方法。

在命令行调用 npm run dev 执行编译。

Vue 组件

laravel mix 包罗万象,支持 vue 组件编译,如果你不使用 vue 的话,可以忽略这块。

单文件组件是 vue 最重要的特征。在一个文件里为一个组件定义模板,脚本,样式表。

./resources/assets/js/app.js

1
2
3
4
5
6
7
import Vue from "vue";
import Notification from "./components/Notification.vue";

new Vue({
el: "#app",
components: { Notification },
});

在上面,我们导入了 vue(首先你需要执行npm install vue --save-dev安装 vue),然后引入了一个叫 Notification 的 vue 组件并且注册了 root vue 实例。

./resources/asset/js/components/Notification.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="notification">
{{ body }}
</div>
</template>

<script>
export default {
data() {
return {
body: 'I am a notification.'
}
}
}
</script>
<style>
.notification {
background: grey;
}
</style>

如果你了解 vue,这些你都会很熟悉,继续。

./webpack.mix.js

1
2
3
let mix = require('laravel-mix');

mix.js('resources/assets/js/app.js', 'public/js');

执行 npm run dev 编译文件,这样就简单的创建了一个 HTML 文件,引入 ./js/app.js 文件,然后在浏览器里查看吧!

React 支持

laravel mix 也装载了基本的 react 支持,只要把 mix.js()改成 mix.react()并且使用相同的参数。在底层,mix 会引用 react 需要的任何 babel 插件。

1
mix.react('resources/assets/js/app.jsx', 'public/js/app.js');

当然,你仍然需要使用 npm 安装 react 和 reactDOM,不过要注意小心行事。

Typescript 支持

Laravel Mix 还附带基本的 Typescript 支持。
只需更新您的 mix.js() 调用 mix.ts(),然后使用完全相同的参数集。

1
mix.ts('resources/assets/js/app.ts', 'public/js/app.js');

当然,你仍然希望做必要的调整,如创建tsconfig.json文件和安装DefinitelyTyped,但其他一切都应该在开发郭晨他提供中考虑到了。

库代码分离

1
mix.js(src, output).extract();

把所有的 js 都打包成一个文件会伴随着潜在的风险:每次更新项目中就算很小的一部分都需要破坏所有用户的缓存,这意味着你的第三方库需要重新被下载和缓存。这样很不好。

一个解决的办法是分离或者提取你的库文件。

  • 应用代码: app.js
  • vendor 库: vendor.js
  • Manifest(webpack runtime): manifest.js
1
mix.extract();

or

1
mix.extract(['vue', 'jquery']);

extract 方法接受一个你想要从打包文件里提取出的库的数组,使用这个方法,Vue 和 jQuery 的源代码都会被放在 vendor.js 里。如果在未来你需要对应用代码做一些微小的变动,并不会对大的 vendor 库产生影响,它们依然会留在长期缓存。

一旦执行 webpack 打包文件,你会发现三个新的文件,你可以在 HTML 页面引用它们。

1
2
3
<script src="/js/manifest.js"></script>
<script src="/js/vendor.js"></script>
<script src="/js/app.js"></script>

实际上,我们付出了一些 HTTP 请求的代价(就是会多请求几次)来换取长期缓存的提升。

Manifest 文件是什么

webpack 编译的时候会有一些 run-time 代码协助其工作。如果没有使用 mix.extract(),这些代码你是看不到的,它会在打包文件里,然而,如果我们分离了代码并且允许长期缓存,在某些地方就需要这些 run-time 代码,所以,mix 会把它提取出来,这样一来,vendor 库和 manifest 文件都会被缓存很长时间。

浏览器自动刷新

1
mix.browserSync('my-site.test');

BrowserSync 能自动监控文件变动并且把你的变化通知浏览器, – 完全不需要手动刷新。你可以调用 mix.browserSync() 方法来开启这个功能:

1
2
3
4
5
6
7
8
mix.browserSync('my-domain.test');

// Or:

// https://browsersync.io/docs/options/
mix.browserSync({
proxy: 'my-domain.test'
})

参数可以传字符串(proxy),也可以传对象(BrowserSync 设置)。你声明的域名作为 proxy 是非常重要的,Browsersync 将通过代理网址来输出到你的虚拟主机(webpack dev server).

其他选项可以从 Browsersync Documentation

现在, 启动 dev server (npm run watch), 并进行下一步操作吧.

模块热替换

laravel mix 对模块热替换提供了无缝的支持。

模块热替换(或者叫热加载),意思就是当 javascript 改变刷新页面的时候可以维持组件的状态,例如现在有一个计数器,按一下按钮,计数器会加 1,想象一下你点了很多次然后修改一下组件的相关文件,浏览器会实时的反映出你所做出的更改而保持计数器不变,计数器不会被重置,这就是热加载的意义最在。

在 laravel 里的用法

Laravel 和 Laravel 一起工作, 来抽象出热加载的复杂性.

看一下 laravel 里的 package.json 文件,在 scripts 模块,你可以看到:

1
2
3
4
5
6
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --progress --hide-modules",
"watch": "cross-env NODE_ENV=development webpack --watch --progress --hide-modules",
"hot": "cross-env NODE_ENV=development webpack-dev-server --inline --hot",
"production": "cross-env NODE_ENV=production webpack --progress --hide-modules"
}

注意一下 hot 选项,这个地方就是你所需要的,在命令行执行 npm run hot 会启动一个 node 服务器并且监视你的 bundle 文件,接下来,在浏览器打开你的 laravel 应用,一般应该是 http://my-app.test

在 laravel 应用里使用热加载很重要的一点是要保证所有的脚本资源引用的是前面启动的 node 服务器的 url:http://localhost:8080,现在你可以手动更新你的 HTML\Blade 文件了:

1
2
3
4
<body>
<div id="app">...</div>
<script src="http://localhost:8080/js/bundle.js"></script>
</body>

假设你有一些组件,尝试在浏览器里更改他们的状态,然后更新他们的模板文件,你可以看到浏览器会立刻反应出你的更新,但是状态并没有被改变。

但是,在开发部署环境下手动更新 url 会是一个负担,所以,laravel 提供了一个 mix()方法,他会动态的构建 js 或者样式表的引用,然后输出。上面的代码因此可以修改成:

1
2
3
4
5
<body>
<div id="app"></div>

<script src="{{ mix('js/bundle.js') }}"></script>
</body>

调整之后,Laravel 将为你做这项工作。如果运行 npm run hot 以启用热重加载,则该函数将设置必要的 http://localhost:8080 作为 URL。相反,如果您使用 npm run devnpm run pro,它将使用域名作为基准 url。

在 Https 中使用
如果你在 HTTPS 连接上开发你的应用,你的热重加载脚本和样式也必须通过 HTTPS 服务。要实现这一点,可以将 -—https 标志添加到 package.json 中的热选项命令中。

1
2
3
"scripts": {
"hot": "NODE_ENV=development webpack-dev-server --inline --hot --https",
}

通过上面的设置,webpack-dev-server 将会生成一个自签名证书。如果你希望使用自己的证书,可以使用以下设置:

1
"hot": "NODE_ENV=development webpack-dev-server --inline --hot --https --key /path/to/server.key --cert /path/to/server.crt --cacert /path/to/ca.pem",

现在在你的 Html/Blade 文件中可以使用

1
<script src="https://localhost:8080/js/bundle.js"></script>

或者

1
<script src="{{ mix('js/bundle.js') }}"></script>

在 spa 里的用法

laravel mix 包含了流行的 vue-loader 包,这意味着,如果是单页应用,你什么都不需要做,它是开箱即用的。

版本化

1
2
mix.js('src', 'output')
.version([]);

为了帮助长期缓存,Laravel Mix 提供了 mix.version() 方法,它支持文件散列。比如app.js?id=8e5c48eadbfdd5458ec6。这对清除缓存很有用。假设你的服务器自动缓存了一年的脚本,以提高性能。这很好,但是,每当您对应用程序代码进行更改时,需要一些方法来告诉用户更新缓存, 这通常是通过使用查询字符串或文件哈希来完成的。

启用了版本控制之后,每次代码更改时,都会生成一个新的散列查询字符串文件。看以下webpack.mix.js 文件

编译后,你会在 mix-manifest.json 文件看到 /css/app.css?id=5ee7141a759a5fb7377a/js/app.js?id=0441ad4f65d54589aea5。当然,你的特定散列将是唯一的。每当你调整 JavaScript 时,编译后的文件将会收到一个新的散列名称,这将有效地破坏缓存,一旦被推到生产环境中。

举个例子,试试 webpack --watch,然后修改一下你的 JavaScript。你将立即看到一个新生成的打包文件和样式表。

导入版本文件

这就引出了一个问题:如果名称不断变化,我们如何将这些版本化的脚本和样式表包含到 HTML 中呢?是的,这很棘手。答案将取决于你构建的应用程序的类型。对于 SPA,你可以动态地读取 Laravel Mix 生成的 manifest.json 文件,提取资料文件名(这些名称将被更新,以反映新的版本文件),然后生成 HTML。

Laravel 用户

对于 Laravel 项目,一个解决方案是开箱即用的。只需调用全局 mix() 函数,就完成了!我们将计算出导入的适当文件名。这里有一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>App</title>
<link rel="stylesheet" href="{{ mix('css/app.css') }}" />
</head>
<body>
<div id="app">
<h1>Hello World</h1>
</div>

<script src="{{ mix('js/app.js') }}"></script>
</body>
</html>

将未散列的文件路径传递给 mix() 函数,然后在后端,我们将弄清楚应该导入哪个脚本或样式表。请注意,你可能/应该使用这个函数,即使没有对文件进行版本控制。

版本化附加文件

mix.version() 将自动生成编译后的 JavaScript、Sass/Less 或合并文件。但是,如果你还希望将额外的文件作为构建的一部分,简单地传递路径或路径数组,就像这样:

1
mix.version(['public/js/random.js']);

现在,我们会版本化任何相关的编译文件,但是我们还会附加一个查询字符串,public/js/random.js?{hash},并更新 mix-manifest.json 文件。

Css 预处理器

1
2
3
4
5
mix.sass('src', 'output', pluginOptions);
mix.standaloneSass('src', 'output', pluginOptions); // Isolated from Webpack build.
mix.less('src', 'output', pluginOptions);
mix.stylus('src', 'output', pluginOptions);
mix.postCss('src', 'output', [ require('precss')() ])

一个单一的方法调用允许你编译你的 Sass,Less,或 Stylus 文件,同时自动应用 CSS3 前缀。

虽然 webpack 可以将所有 CSS 直接插入到绑定的 JavaScript 中,但 Laravel Mix 会自动执行必要的步骤,将其提取到你想要的输出路径中。

多构建

如果你需要编译多个顶级文件,你可以根据需要调用 mix.sass()(或任何一个预处理器变体). 对于每个调用,webpack 将输出一个包含相关内容的新文件。

1
2
mix.sass('src/app.scss', 'dist/') // creates 'dist/app.css'
.sass('src/forum.scss', 'dist/'); // creates 'dist/forum.css'

例子

让我们看一个例子

webpack.mix.js

1
2
3
let mix = require('laravel-mix');

mix.sass('resources/assets/sass/app.sass', 'public/css');

./resources/assets/sass/app.sass

1
2
3
4
$primary: grey

.app
background: $primary

Tip : 对于 Sass 编译, 你可以使用 .sass.scss 语法

像往常一样运行 npm run webpack 进行编译. 你会发现 ./public/css/app.css 文件包含

1
2
3
.app {
background: grey;
}

插件选项

编译的时候, Laravel Mix 的首先是去分别的调用 Node-Sass, Less,和 Slylus 来编译你的 Sass, Less 文件。有时,你可能需要重写我们传递给它们的默认选项。可以将它们作为第三个参数提供给 mix.sass(), mix.less()mix.stylus()

Stylus 插件
如果使用 Stylus, 你可能希望安装额外的插件,比如 Rupture 。运行 npm install rupture 来安装这个插件,然后在你的 mix.stylus() 中调用, 例如:

1
2
3
4
5
mix.stylus('resources/assets/stylus/app.styl', 'public/css', {
use: [
require('rupture')()
]
});

如果希望更深一步,并且在全局中自动导入插件,您可以使用 import 选项。这里有一个例子:

1
2
3
4
5
6
7
8
9
10
11
mix.stylus('resources/assets/stylus/app.styl', 'public/css', {
use: [
require('rupture')(),
require('nib')(),
require('jeet')()
],
import: [
'~nib/index.styl',
'~jeet/jeet.styl'
]
});

就是这个样子滴!

CSS url() 重写

一个关键的 webpack 概念是,它将重写你的样式表中的任何 url()。虽然这可能最初听起来很奇怪,但它是一项非常强大的功能。

一个例子

假设我们想要编译一些 Sass,其中包含一个图像的相对 url。

1
2
3
.example {
background: url("../images/thing.png");
}

提示:url()的绝对路径将被排除在 url 重写之外。因此,url('/images/thing.png')url('http://example.com/images/thing.png') 将不会被更改。

注意,这里说的是相对 URL? 默认情况下,Laravel Mix 和 webpack 将会找到 thing.png ,将其复制到 public/images 文件夹中,然后在生成的样式表中重写 url()。因此,编译的 CSS 将是:

1
2
3
.example {
background: url(/images/thing.png?d41d8cd98f00b204e9800998ecf8427e);
}

这也是 webpack 的一个很酷的特性。然而,它确实有一种倾向,让那些不理解 webpack 和 css-loader 插件如何工作的人感到困惑。你的文件夹结构可能已经是您想要的了,而且你希望不要修改那些url()。如果是这样的话,我们确实提供了一个覆盖方式:

1
2
3
4
mix.sass('src/app.scss', 'dist/')
.options({
processCssUrls: false
});

把这个加上你的 webpack.mix.js 文件中,我们将不再匹配 url() 或复制资源到你的公共目录。因此,编译后的 CSS 将与你输入时一样:

1
2
3
.example {
background: url("../images/thing.png");
}

这样的好处是,当禁用 url 处理时,您的 Webpack, Sass 编译和提取可以更快地编译。

PostCSS 插件

默认情况下,Mix 将通过流行的 Autoprefixer PostCSS plugin 将所有的 CSS 文件连接起来。因此,你可以自由使用最新的 CSS 3 语法,并理解我们将自动应用任何必要的浏览器前缀。在大多数情况下,默认设置应该就可以,但是,如果你需要调整底层的自动修复程序配置,那么如下所示:

1
2
3
4
5
6
7
8
9
10
mix.sass('resources/assets/sass/app.scss', 'public/css')
.options({
autoprefixer: {
options: {
browsers: [
'last 6 versions',
]
}
}
});

另外,如果你想完全禁用它——或者依赖一个已经包含 自动前缀的 PostCSS 插件:

1
2
mix.sass('resources/assets/sass/app.scss', 'public/css')
.options({ autoprefixer: false });

但是,你可能想要在构建中应用额外的 PostCSS 插件。木问题, 只需在 NPM 中安装所需的插件,然后在 webpack.mix.js 文件中引用,如下所示:

1
2
3
4
5
6
mix.sass('resources/assets/sass/app.scss', 'public/css')
.options({
postCss: [
require("postcss-custom-properties")
]
});

完成了!现在可以使用和编译自定义 CSS 属性(如果这是您的东西)。例如,如果 resources/assets/sass/app.scss 包含…

1
2
3
4
5
6
7
:root {
--some-color: red;
}

.example {
color: var(--some-color);
}

编译完成将会是

1
2
3
.example {
color: red;
}

PostCss 不适用 Sass/Less

或者,如果你更喜欢跳过 Sass/Less/Stylus 编译步骤,而是使用 PostCSS,你可以通过mix.postCss() 方法来完成。

1
2
3
mix.postCss('resources/assets/css/main.css', 'public/css', [
require('precss')()
]);

请注意,第三个参数是应该应用于你构建的 postcss 插件的数组。

独立 Sass 构建

如果你不希望 Mix 和 Webpack 以任何方式处理你的 Sass 文件,你可以使用mix.standaloneSass(),这将大大改善你应用程序的构建时间。请记住:如果你选择了这条路线,Webpack 不会触及你的 CSS。它不会重写 url,复制资源(通过 file-loader),或者应用自动图像优化或 CSS 清理。如果这些特性对于你的应用程序来说是没有必要的,那么一定要使用这个选项而不是mix.sass()

1
mix.standaloneSass('resources/assets/sass/app.scss', 'public/css');

注意:如果你正在使用 standaloneSass,在使用 npm run watch 进行文件更改时,你将需要使用下划线来前缀导入的文件,以将它们标记为依赖文件(例如,_header.scss _alert.scss)。如果不这样做,将导致 Sass 编译错误和/或额外的 CSS 文件。

文件复制

1
2
3
4
mix.copy(from, to);
mix.copy('from/regex/**/*.txt', to);
mix.copy([path1, path2], to);
mix.copyDirectory(fromDir, toDir);

有时, 你需要复制一个或者多个文件作为构建过程的一部分。没有问题, 这是小事一桩。使用mix.copy() 方法指定源文件或文件夹,然后指定您想要的目标文件夹/文件

1
mix.copy('node_modules/vendor/acme.txt', 'public/js/acme.txt');

在编译时,’acme’ 文件将被复制到 ‘public/js/acme.txt’。这是一个常见的用例,当你希望将一组字体通过 NPM 安装到 public 目录时。

系统通知

默认情况下,Laravel Mix 将显示每个编译的系统通知。这样,你就可以快速查看是否有需要查询的错误。但是,在某些情况下,这是不可取的(例如在生产服务器上编译)。如果发生这种情况,它们可以从你的 webpack.mix.js 文件中禁用。

1
2
mix.js(src, output)
.disableNotifications();

文件组合和最小化

1
2
3
4
mix.combine(['src', 'files'], 'destination');
mix.babel(['src', 'files'], destination);
mix.minify('src');
mix.minify(['src']);

如果使用得当,Laravel Mix 和 webpack 应该负责所有必要的模块捆绑和最小化。但是,你可能有一些遗留代码或第三方库需要连接和最小化, 这并不是一个问题。

组合文件

考虑下面的代码片段:

1
mix.combine(['one.js', 'two.js'], 'merged.js');

这自然会合并 one.jstwo.js 到一个单独的文件,叫做 merged.js。与往常一样,在开发期间,合并文件将保持未压缩状态。但是,对于生产(export NODE_ENV=production),这个命令将会最小化 merged.js

组合文件与 Babel 编译。

如果需要组合 使用 ES2015 方法编写的 JavaScript 文件,你可以更新的 mix.combine() 调用 mix.babel()。方法签名相同。唯一的区别是,在将文件组合起来之后,Laravel Mix 将对结果进行 Babel 编译,从而将代码转换成所有浏览器都能理解的 JavaScript 代码。

1
mix.babel(['one.js', 'two.js'], 'merged.js');

最小化文件

同样,你也可以使用 mix.minify() 命令缩小一个或多个文件。

1
2
mix.minify('path/to/file.js');
mix.minify(['this/one.js', 'and/this/one.js']);

这里有一些值得注意的事情:

  • 该方法将创建一个额外的 *.min.ext 文件。因此,压缩 app.js 将生成 app.min.js
  • 再一次声明,压缩只会在生产过程中发生。(export NODE_ENV=production)。
  • 不需要调用 mix.combine(['one.js', 'two.js'], 'merged.js').minify('merged.js'); ,只使用单一的 mix.combine() 调用。它会兼顾两者。

    重要:请注意,压缩只适用于 CSS 和 JavaScript 文件。minifier 不理解任何其他提供的文件类型。

自动加载

1
2
3
mix.autoload({
jquery: ['$', 'window.jQuery']
});

Webpack 提供了必要的功能,可以在 Webpack 所要求的每个模块中把一个模块作为变量。如果你使用的是一个特定的插件或库,它依赖于一个全局变量,例如 jQuery, mix.autoload() 可能会对你有用。

考虑下面的例子:

1
2
3
mix.autoload({
jquery: ['$', 'window.jQuery']
});

该代码片段指定 webpack 应该将 var $ = require('jquery') 添加到它所遇到的全局$标识符或 window.jQuery 中。漂亮!

事件钩子

1
mix.then(function () {});

你可能需要监听每次 webpack 完成编译的事件。也许你需要手动应用一些适合你的应用程序的逻辑。如果是这样,您可以使用 mix.then() 方法来注册任何回调函数。这里有一个例子:

1
2
3
4
mix.js('resources/assets/js/app.js', 'public/js')
.then(() => {
console.log('webpack has finished building!');
});

回调函数将通过 webpack Stats 对象,允许对所执行的编译进行检查:

1
2
3
4
5
mix.js('resources/assets/js/app.js', 'public/js')
.then((stats) => {
// array of all asset paths output by webpack
console.log(Object.keys(stats.compilation.assets));
});

可以在这里找到 Stats 对象的官方文档 : https://github.com/webpack/docs/wiki/node.js-api#stats

快速 webpack 配置

1
mix.webpackConfig({} || cb);

当然,你可以自由编辑提供的 webpack.config.js 文件,在某些设置中,更容易直接从你的 webpack.mix.js 修改或覆盖默认设置。对于 Laravel 应用来说尤其如此,默认情况下是在项目根文件夹中没有 webpack.config.js

例如,你可能希望添加一个由 webpack 自动加载的模块的自定义数组。在这个场景中,您有两个选项:

  • 根据需要编辑你的 webpack.config.js 文件
  • 在你的 webpack.mix.js 中调用 mix.webpackConfig() 文件,并传递重写参数。然后混合将进行一个深度合并。
    下面,作为一个示例,我们将为 Laravel Spark 添加一个自定义模块路径。
1
2
3
4
5
6
7
8
mix.webpackConfig({
resolve: {
modules: [
'node_modules',
path.resolve(__dirname, 'vendor/laravel/spark/resources/assets/js')
]
}
});

使用回调函数

当传递回调函数时,你可以访问 webpack 及其所有属性。

1
2
3
4
5
6
7
8
9
10
11
mix.webpackConfig(webpack => {
return {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery',
})
]
};
});

扩展 Mix

基于组件的系统 Mix 使用场景构建它的 API,你也可以访问—是否为你的项目扩展 Mix,或者作为一个可重用的包分发到世界。

可以在扩展页面找到已有的扩展列表。

例子

1
2
3
4
5
6
7
8
9
// webpack.mix.js;
let mix = require('laravel-mix');

mix.extend('foo', function(webpackConfig, ...args) {
console.log(webpackConfig); // the compiled webpack configuration object.
console.log(args); // the values passed to mix.foo(); - ['some-value']
});

mix.js('src', 'output').foo('some-value');

在上面的示例中,我们可以看到 mix.extend() 接受两个参数:在定义组件时应该使用的名称,以及一个回调函数或类,这些函数注册并组织必要的 webpack 逻辑。在后台,一旦构建了底层 webpack 配置对象,Mix 将触发这个回调函数。这将给你一个机会来插入或覆盖任何必要的设置。

虽然简单的回调函数可能对快速扩展很有用,但在大多数情况下,您可能希望构建一个完整的组件类,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mix.extend(
'foo',
new class {
register(val) {
console.log('mix.foo() was called with ' + val);
}

dependencies() {}

webpackRules() {}

webpackPlugins() {}

// ...
}()
);

在扩展 Mix 时,通常需要触发一些指令:

  • 安装这些依赖关系。
  • 将此规则/加载程序添加到 webpack 中。
  • 包含这个 webpack 插件。
  • 完全覆盖 webpack 配置的这一部分。
  • 将此配置添加到 Babel。
  • 等。

这些操作中的任何一个都是带有 Mix 组件系统有联系。

组件的接口

  • name:当调用组件时,应该使用什么作为方法名。(默认为类名。)
  • dependencies:列出应该由 Mix 安装的所有 npm 依赖项。
  • register:当您的组件被调用时,所有的用户参数将立即被传递给这个方法。
  • boot:启动组件。这个方法是在用户的 webpack.mix 之后触发的。js 文件已经加载完毕。
  • webpackEntry:附加到主混合 webpack 入口对象。
  • webpackRules:与主 webpack 加载器合并的规则。
  • webpackplugin:与主 webpack 配置合并的插件。
  • webpackConfig:覆盖生成的 webpack 配置。
  • babelConfig:额外的 Babel 配置应该与 Mix 的默认值合并。

这里有一个示例/虚拟组件,它可以让你更好地了解如何构建自己的组件。更多的例子,请参考在后台 Mix 使用的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
class Example {
/**
* The optional name to be used when called by Mix.
* Defaults to the class name, lowercased.
*
* Ex: mix.example();
*
* @return {String|Array}
*/
name() {
// Example:
// return 'example';
// return ['example', 'alias'];
}

/**
* All dependencies that should be installed by Mix.
*
* @return {Array}
*/
dependencies() {
// Example:
// return ['typeScript', 'ts'];
}

/**
* Register the component.
*
* When your component is called, all user parameters
* will be passed to this method.
*
* Ex: register(src, output) {}
* Ex: mix.yourPlugin('src/path', 'output/path');
*
* @param {*} ...params
* @return {void}
*
*/
register() {
// Example:
// this.config = { proxy: arg };
}

/**
* Boot the component. This method is triggered after the
* user's webpack.mix.js file has executed.
*/
boot() {
// Example:
// if (Config.options.foo) {}
}

/**
* Append to the master Mix webpack entry object.
*
* @param {Entry} entry
* @return {void}
*/
webpackEntry(entry) {
// Example:
// entry.add('foo', 'bar');
}

/**
* Rules to be merged with the master webpack loaders.
*
* @return {Array|Object}
*/
webpackRules() {
// Example:
// return {
// test: /\.less$/,
// loaders: ['...']
// });
}

/*
* Plugins to be merged with the master webpack config.
*
* @return {Array|Object}
*/
webpackPlugins() {
// Example:
// return new webpack.ProvidePlugin(this.aliases);
}

/**
* Override the generated webpack configuration.
*
* @param {Object} webpackConfig
* @return {void}
*/
webpackConfig(webpackConfig) {
// Example:
// webpackConfig.resolve.extensions.push('.ts', '.tsx');
}

/**
* Babel config to be merged with Mix's defaults.
*
* @return {Object}
*/
babelConfig() {
// Example:
// return { presets: ['@babel/preset-react'] };
}
}

请注意,上面示例中的每个方法都是可选的。在某些情况下,您的组件可能只需要添加一个 webpack 加载程序和/或调整混合使用的 Babel 配置。没有问题的话省略其余的接口。

1
2
3
4
5
6
7
8
class Example {
webpackRules() {
return {
test: /\.test$/,
loaders: []
};
}
}

现在,当 Mix 构造底层 webpack 配置时,你的规则将包含在生成的webpackConfig.module.rules 数组中。

使用

一旦你构建或安装了想要的组件,只需从 webpack.mix.js 中获取它即可,你都准备好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// foo-component.js

let mix = require('laravel-mix');

class Example {
webpackRules() {
return {
test: /\.test$/,
loaders: []
};
}
}

mix.extend('foo', new Example());
1
2
3
4
5
6
7
8
9
// webpack.mix.js

let mix = require('laravel-mix');
require('./foo-component');

mix
.js('src', 'output')
.sass('src', 'output')
.foo();

自定义方法

LiveReload

现在 Laravel Mix 与 Browsersync 已经支持了开箱即用,但是你可能更喜欢使用 LiveReload, 当检测到修改时,LiveReload 可以自动监视您的文件并刷新页面。

安装 webpack-livereload-plugin

1
npm install webpack-livereload-plugin@1 --save-dev

配置 webpack.mix.js

将以下几行添加到 webpack.mix.js 底部。

1
2
3
4
5
6
7
var LiveReloadPlugin = require('webpack-livereload-plugin');

mix.webpackConfig({
plugins: [
new LiveReloadPlugin()
]
});

虽然 LiveReload 有她很好用的默认值,但是这里可以查看一个可用的插件选项列表

安装 LiveReload.js

最后,我们需要安装 LiveReload.js。您可以通过 LiveReload Chrome 插件,或者在你的主要站点模板的关闭</body>标记之前添加以下代码:

1
2
3
@if(config('app.env') == 'local')
<script src="http://localhost:35729/livereload.js"></script>
@endif

运行 dev server

1
npm run watch

现在,LiveReload 将自动监控您的文件并在必要时刷新页面。享受吧!

Jquery UI

jQuery UI 是一个用于呈现公共组件的工具包,比如 datepickers、draggables 等。不需要做任何调整,以使其与 Laravel Mix 一起工作。

构建 webpack.mix.js 配置

1
2
mix.js('resources/assets/js/app.js', 'public/js')
.sass('resources/assets/sass/app.scss', 'public/css');

安装 jquery-ui

1
npm install jquery-ui --save-dev

加载必要插件

1
2
3
4
5
6
// resources/assets/js/app.js

import $ from 'jquery';
window.$ = window.jQuery = $;

import 'jquery-ui/ui/widgets/datepicker.js';

加载 CSS

1
2
3
// resources/assets/sass/app.scss

@import '~jquery-ui/themes/base/all.css';

触发 UI 组件

1
2
// resources/assets/js/app.js
$('#datepicker').datepicker();

高级配置

Laravel Mix 配置项

1
2
3
4
5
6
7
8
9
10
11
12
mix.options({
extractVueStyles: false,
processCssUrls: true,
terser: {},
purifyCss: false,
//purifyCss: {},
postCss: [require('autoprefixer')],
clearConsole: false,
cssNano: {
// discardComments: {removeAll: true},
}
});

如果需要的话可以使用一些混合选项和覆盖选项。请注意上面的选项,以及它们的默认值。这里有一个快速概述:

  • **extractVueStyles:**提取 .vue 组件样式(CSS 在 <style> 标签内)到一个专用文件,而不是将其嵌入到 HTML 中。
  • **globalVueStyles:**表示一个文件包含在每个组件样式中。这个文件应该只包含变量、函数或 mixin,以便在最终的编译文件中防止重复的 css。这个选项只有在启用了提取工具时才有效。
  • **processCssUrls:**进程/优化相对样式表url()。默认情况下,Webpack 会自动更新这些 url。但是,如果您的文件夹结构已经按照您想要的方式进行组织,那么将此选项设置为 false 以禁用处理。
  • terser: 使用此选项合并项目所需的任何自定义 Terser 选项
  • **purifyCss:**如果你想要混合自动读取你的 HTML/Blade 文件,并删除你的 CSS 包,你可以将这个选项设置为 true。您还可以传递包含 purifycss-webpack 选项的对象。
  • **postCss:**合并任何自定义的 postCss 插件。
  • **clearConsole:**设置为 false,如果您不想在每次构建后清除终端/控制台。
  • cssNano: 使用此选项设置项目所需的cssnano 选项

开发协议

MIT

Copyright (c) 2018 Jeffrey Way jeffrey@jeffrey-way.com

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

[转] Laravel5 插件包 vendor 开发

原文地址: laravel5.2插件包vendor开发

准备工作

1
2
3
4
1)拥有git账户密码,熟悉git常用命令
2)拥有packagist账户密码
3)本地安装了composer
4)必须laravel5.2版本且有一定的基础,了解服务提供者/门面概念

1)新建文件夹

在新建的laravel项目中建立如下目录:

1
packages/yuansir/toastr/src

packages 目录和 app 目录同级。
我们开发包的代码都放在这个src目录中,yuansir和toastr完全自定义。


2)修改项目根目录的composer.json

修改项目的composer.json,设定PSR-4命名空间:

1
2
3
4
5
6
7
8
9
"autoload": {
"classmap": [
"database"
],
"psr-4": {
"App\\": "app/",
"Yuansir\\Toastr\\": "packages/yuansir/toastr/src/"
}
},

3)重新生成autoload文件

根目录下cmd执行

1
composer dump-autoload

4)扩展包composer.json

cmd切换到插件目录:packages/yuansir/toastr 执行命令,根据提示填写

1
composer init

填写完基本信息之后 在packages/geekghc/laraflash目录下就会生成一个composer.json文件


5)创建服务提供者Service Provider

1
php artisan make:provider ToastrServiceProvider

将生成的app/Providers/ToastrServiceProvider.php文件移动到我们的packages/yuansir/toastr/src 目录下面,并注册ToastrServiceProvider到config/app.php 的providers 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
'providers' => [

/*
* Laravel Framework Service Providers...
*/
......

/*
* Application Service Providers...
*/
......
Yuansir\Toastr\ToastrServiceProvider::class,
],

6)创建配置文件

新建packages/yuansir/toastr/src/config/toastr.php 来保存toastr.js的options

1
2
3
4
<?php
return [
'options' => []
];

7)创建自定义类

新建Toastr类,来实现toastr 的info,success,error,warning的相关实现,代码还是很简单的,packages/yuansir/toastr/src/Toastr.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<?php namespace Yuansir\Toastr;
use Illuminate\Session\SessionManager;
use Illuminate\Config\Repository;
class Toastr
{
/**
* @var SessionManager
*/
protected $session;

/**
* @var Repository
*/
protected $config;

/**
* @var array
*/
protected $notifications = [];

/**
* Toastr constructor.
* @param SessionManager $session
* @param Repository $config
*/
public function __construct(SessionManager $session, Repository $config)
{
$this->session = $session;
$this->config = $config;
}

public function render()
{
$notifications = $this->session->get('toastr:notifications');

if(!$notifications) {
return '';
}

foreach ($notifications as $notification) {
$config = $this->config->get('toastr.options');
$javascript = '';
$options = [];
if($config) {
$options = array_merge($config, $notification['options']);
}

if($options) {
$javascript = 'toastr.options = ' . json_encode($options) . ';';
}

$message = str_replace("'", "\\'", $notification['message']);
$title = $notification['title'] ? str_replace("'", "\\'", $notification['title']) : null;
$javascript .= " toastr.{$notification['type']}('$message','$title');";
}

return view('Toastr::toastr', compact('javascript'));
}

/**
* Add notification
* @param $type
* @param $message
* @param null $title
* @param array $options
* @return bool
*/
public function add($type, $message, $title = null, $options = [])
{
$types = ['info', 'warning', 'success', 'error'];
if(!in_array($type, $types)) {
return false;
}

$this->notifications[] = [
'type' => $type,
'title' => $title,
'message' => $message,
'options' => $options
];
$this->session->flash('toastr:notifications', $this->notifications);
}

/**
* Add info notification
* @param $message
* @param null $title
* @param array $options
*/
public function info($message, $title = null, $options = [])
{
$this->add('info', $message, $title, $options);
}

/**
* Add warning notification
* @param $message
* @param null $title
* @param array $options
*/
public function warning($message, $title = null, $options = [])
{
$this->add('warning', $message, $title, $options);
}

/**
* Add success notification
* @param $message
* @param null $title
* @param array $options
*/
public function success($message, $title = null, $options = [])
{
$this->add('success', $message, $title, $options);
}

/**
* Add error notification
* @param $message
* @param null $title
* @param array $options
*/
public function error($message, $title = null, $options = [])
{
$this->add('error', $message, $title, $options);
}

/**
* Clear notifications
*/
public function clear()
{
$this->notifications = [];
}
}

8)创建视图文件

新建 packages/yuansir/toastr/src/views/toastr.blade.php 视图文件:

1
2
3
<link href="http://cdn.bootcss.com/toastr.js/latest/css/toastr.min.css" rel="stylesheet">
<script src="http://cdn.bootcss.com/toastr.js/latest/js/toastr.min.js"></script>
<script type="text/javascript">{!! $javascript !!}</script>

9)创建门面Facade

建立Facade,新建packages/yuansir/toastr/src/Facades/Toastr.php 就是引入了tastr插件,输出我们render方法中的$javascript

1
2
3
4
5
6
7
8
9
<?php namespace Yuansir\Toastr\Facades;
use Illuminate\Support\Facades\Facade;
class Toastr extends Facade
{
protected static function getFacadeAccessor()
{
return 'toastr';
}
}

10)修改服务提供者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php namespace Yuansir\Toastr;

use Illuminate\Support\ServiceProvider;

class ToastrServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
$this->loadViewsFrom(__DIR__ . '/views', 'Toastr');

$this->publishes([
__DIR__.'/views' => base_path('resources/views/vendor/toastr'),
__DIR__.'/config/toastr.php' => config_path('toastr.php'),
]);
}

/**
* Register the application services.
*
* @return void
*/
public function register()
{
$this->app['toastr'] = $this->app->share(function ($app) {
return new Toastr($app['session'], $app['config']);
});
}

/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return ['toastr'];
}
}

$this->loadViewsFrom( DIR . ‘/views’, ‘Toastr’); 就是表示Toastr命名空间的视图文件冲当前目录的views目录中渲染,所以我们上面用 return view(‘Toastr::toastr’, compact(‘javascript’));

$this->publishes 在执行php artisan vendor:publish 时会将对应的目录和文件复制到对应的位置


11)本地测试

修改 config/app.php 添加服务提供者如下:

1
2
3
4
5
6
'aliases' => [
......

'Toastr' => Yuansir\Toastr\Facades\Toastr::class,

],

创建测试控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
php artisan make:controller TestController

<?php

namespace App\Http\Controllers;

use App\Http\Requests;
use Illuminate\Http\Request;
use Toastr;

class HomeController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
//略
}

/**
* Show the application dashboard.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
Toastr::error('你好啊','标题');
dd(session('toastr:notifications'));
return view('home');
}
}

到此结束,大功告成,这样一个Laravel 的 composer 包就开发完成了。

修改命名空间到包的composer.json,因为别人安装这个包的时候不可能也去改项目composer.json的PSR-4的autoload,所以我们把PSR-4的命名空间加到这个包的composer.json中去,修改packages/yuansir/toastr/src/composer.json 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "ryan/toastr-for-laravel",
"description": "toastr.js for laravel5",
"authors": [
{
"name": "Ryan",
"email": "yuansir@live.cn"
}
],
"require": {},
"autoload": {
"psr-4": {
"Yuansir\\Toastr\\": "src/"
}
}
}

12)发布到git上

发布到自己git上:例如Github项目 或者码云: git窗口中命令如下

1
2
3
4
git add .
git status
git commit -m 'vendor'
git push

切记创建版本号:

1
2
$ git tag -a 1.0.0 -m "version 1.0.0"
$ git push --tags

13)发布到packagist上供大家composer安装

提交到Packagist,打开到 packagist.org,登陆后点击右边上角的 submit,并填入git的项目地址git@github.com:yuansir/toastr-for-laravel5.git 点击 check 就OK了


14)别人安装使用

切记翻墙才能加载

1
composer require aaa/bbb (aaa/bbb是你composer.json中的name值)

1
2
3
4
Run composer require ryan/toastr-for-laravel
Add Yuansir\Toastr\ToastrServiceProvider::class, to providers in config/app.php
Add 'Toastr' => Yuansir\Toastr\Facades\Toastr::class, to aliases in config/app.php
Run php artisan vendor:publish


15)Demo视图

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<title>Laravel</title>
</head>
<body>
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>
{!! Toastr::render() !!}
</body>
</html>


16)Demo

教程的源码和这个包的安装使用方法详见github https://github.com/yuansir/toastr-for-laravel5


[参考资料]
Laravel Composer Package 开发简明教程
Laravel Composer Package 开发简明教程2

laravel 的 名言警句 Inspiring && Collection

首先有几项配置:

config/app.php

1
2
3
4
5
'alias'=>[
...
'Inspiring' => 'Illuminate\Foundation\Inspiring',
...
]

这个告知 laravel 的使用者, Inspiring的方法是可以在模版中或者框架中直接调用的. 所以框架中的示例文件会 Inspiring::quote()这样调用.

这里边的几个句子是这样的:

1
2
3
4
5
6
7
8
'When there is no desire, all things are at peace. - Laozi',           # 无知无欲 - 老子
'Simplicity is the ultimate sophistication. - Leonardo da Vinci', # 简约是复杂的最终形式 - 达芬奇
'Simplicity is the essence of happiness. - Cedric Bledsoe', # 生活的终极要义就是简单
'Smile, breathe, and go slowly. - Thich Nhat Hanh', # 微笑, 平和, 慢慢自然
'Simplicity is an acquired taste. - Katharine Gerould', # 简单就是美好的味道
'Well begun is half done. - Aristotle', # 好的开始是成功的一半
'He who is contented is rich. - Laozi', # 知足者富
'Very little is needed to make a happy life. - Marcus Antoninus', # 无欲则幸福

这里用到了 Illuminate\Support\Collection, 这是一个数组/对象的初始化组件, 可以对数组进行过滤, 合并, 查找 , 这里 有 api 文档, 感觉类似于 underscore 的 php 版本 里边的函数列表

[译] Laravel-mix 3.0 中文文档

原文地址: Laravel Mix Docs

ps:这个版本是 3.0 文档, 大部分功能和 4.0 一致

概览

基本示例

larave-mix 是位于 webpack 顶层的一个简洁的配置层,在 80% 的情况下使用 laravel mix 会使操作变的非常简单。尽管 webpack 非常的强大,但大部分人都认为 webpack 的学习成本非常高。但是如果你不必用再担心这些了呢?

看一下基本的 webpack.mix.js 文件,让我们想象一下我们现在只需要编译 javascript(ES6)和 sass 文件:

1
2
3
4
let mix = require('laravel-mix');

mix.sass('src/app.sass', 'dist')
.js('src/app.js', 'dist');

怎么样,简单吗?

  1. 编译 sass 文件, ./src/app.sass./dist/app.css
       2. 打包在 ./src/app.js 的所有 js(包括任何依赖)到 ./dist/app.js

使用这个配置文件,可以在命令行触发 webpack 指令:node_modules/bin/webpack

在开发模式下,并不需要压缩输出文件,如果在执行 webpack 的时候加上环境变量:export NODE_ENV=production && webpack,文件会自动压缩

less ?

但是如果你更喜欢使用 Less 而不是 Sass 呢?没问题,只要把 mix.sass() 换成 mix.less()就 OK 了。

使用 laravel-mix,你会使发现大部分 webpack 任务会变得更又把握

安装

尽管 laravel-mix 对于 laravel 使用来说最优的,但也能被用于其他任何应用。

laravel 项目

laravel 已经包含了你所需要的一切,简易步骤:

  1. 安装 laravel

  2. 运行 npm install

  3. 查看 webpack.mix.js 文件 ,就可以开始使用了.

你可以在命令行运行 npm run watch 来监视你的前段资源改变,然后重新编译。

在项目根目录下并没有 webpack.config.js 配置文件,laravel 默认指向根目录下的配置文件。如果你需要自己配置它,你可以把它拷贝到根目录下,同时修改 package.json 里的 npm 脚本: cp node_modules/laravel-mix/setup/webpack.config.js ./.

独立项目

首先使用 npm 或者 yarn 安装 laravel-mix,然后把示例配置文件复制到项目根目录下

1
2
3
4
mkdir my-app && cd my-app
npm init -y
npm install laravel-mix --save-dev
cp -r node_modules/laravel-mix/setup/webpack.mix.js ./

现在你会有如下的目录结构

1
2
3
node_modules/
package.json
webpack.mix.js

webpack.mix.js 是你在 webpack 上层的配置文件,大部分时间你需要修改的是这个文件

首先看下 webpack.mix.js 文件

1
2
3
4
5
let mix = require('laravel-mix');

mix.js('src/app.js', 'dist')
.sass('src/app.scss', 'dist')
.setPublicPath('dist');

注意源文件的路径,然后创建匹配的目录结构(你也可以改成你喜欢的结构)。现在都准备好了,在命令行运行 node_modules/.bin/webpack 编译所有文件,然后你将会看到:

  • dist/app.css
  • dist/app.js
  • dist/mix-manifest.json(你的 asset 输出文件,稍后讨论)

干得漂亮!现在可以干活了。

NPM Scripts

把下面的 npm 脚本添加到你的 package.json 文件中可以加速你的工作操作.,laravel 安装的时候已经包含了这个东西了

1
2
3
4
5
6
"scripts": {
"dev": "NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"hot": "NODE_ENV=development webpack-dev-server --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"production": "NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
}

laravel 工作流程

我们先回顾一下通用的工作流程以便你能在自己的项目上采用

1 . 安装 laravel

1
laravel new my-app

2 . 安装 Node 依赖

1
npm install

**3 . 配置 webpack.mix.js **

这个文件所有前端资源编译的入口

1
2
3
4
let mix = require('laravel-mix');

mix.js('resources/assets/js/app.js', 'public/js');
mix.sass('resources/assets/sass/app.scss', 'public/css');

默认会启用 JavaScript ES2017 + 模块绑定,就行 sass 编译一样。

4 . 编译

用如下指令编译

1
node_modules/.bin/webpack

也可以使用 package.json 里的 npm 脚本:

1
npm run dev

然后会看到编译好的文件:

  • ./public/js/app.js
  • ./public/css/app.css

监视前端资源更改:

1
npm run watch

laravel 自带了一个 ./resources/assets/js/components/Example.vue 文件,运行完成后会有一个系统通知。

5 . 更新视图

laravel 自带一个欢迎页面,我们可以用这个来做示例,修改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Laravel</title>

<link rel="stylesheet" href="{{ mix('css/app.css') }}" />
</head>
<body>
<div id="app">
<example></example>
</div>

<script src="{{ mix('js/app.js') }}"></script>
</body>
</html>

刷新页面,干得漂亮!

常见问题

laravel-mix 必须在 laravel 下使用吗?

不,在 laravel 下使用使最好的,但也可以用在别的任何项目

我的代码没有压缩

只有在 node 环境变量为生产环境时才会被压缩,这样会加速编译过程,但在开发过程中是不必要的,下面是在生成环境下运行 webpack 的示例

1
export NODE_ENV=production && webpack --progress --hide-modules

强烈推荐你把下面的 npm 脚本添加到你的 package.json 文件中,注意 laravel 已经包括了这些了

1
2
3
4
5
6
"scripts": {
"dev": "NODE_ENV=development webpack --progress --hide-modules",
"watch": "NODE_ENV=development webpack --watch --progress --hide-modules",
"hot": "NODE_ENV=development webpack-dev-server --inline --hot",
"production": "NODE_ENV=production webpack --progress --hide-modules"
},

我使用的是 VM,webpack 不能检测到我的文件变化

如果你在 VM 下执行 npm run dev,你会发现 webpack 并不能监视到你的文件改变。如果这样的话,有两种方式来解决这个

  1. 配置 webpack 检测文件系统的变化, 注意:检测文件系统是资源密集型操作并且很耗费电池的使用时长.
  2. 转发文件通过使用类似于 vagrant-fsnotify 之类的东西将通知发送给 VM。注意,这是一个 只有 Vagrant 才有的插件。

检测 VM 文件系统变化, 修改一下你的 npm 脚本,使用 --watch-poll--watch 标签,像这样:

1
2
3
"scripts": {
"watch": "NODE_ENV=development webpack --watch --watch-poll",
}

推送文件改动到 VM, 在主机安装 vagrant-fsnotify

1
vagrant plugin install vagrant-fsnotify

现在你可以配置 vargrant 来使用这个插件, 在 Homestead 中, 在你的 Homestead.yaml 文件类似于这样

1
2
3
4
5
6
7
8
folders:
- map: /Users/jeffrey/Code/laravel
to: /home/vagrant/Code/laravel
options:
fsnotify: true
exclude:
- node_modules
- vendor

一旦你的 vagrant 机器启动, 只需要在主机上运行 vagrant fsnotify 把文件的改动推送到 vm 上, 然后在 vm 内部运行 npm run watch 就能够检测到文件的改动了.

如果你还是有问题,去这儿溜达溜达吧

为什么在我的 css 文件里显示图片在 node_modules 里找不到

你可能用的是相对路径,但是在你的 resources/assets/sass/app.css 里并不存在:

1
2
3
body {
background: url('../img/example.jpg');
}

当引用相对路径的时候,会根据当前文件的路径来搜索,同样的,webpack 会首先搜索 `resources/assets/img/example.jpg ,如果找不到,会继续搜索文件位置,包括 node_modules,如果还找不到,就报错:

1
2
3
ERROR  Failed to compile with 1 errors

This dependency was not found in node_modules:

有两个解决办法:

1 . 让 resources/assets/img/example.jpg 存在这个文件.

2 . 编译 css 的时候添加下面的选项,禁用 css 的 url 处理:

1
2
3
mix.sass("resources/assets/sass/app.scss", "public/css").options({
processCssUrls: false,
});

他对老项目特别有用,因为你的文件夹结构已经完全创建好了。

我不想把 mix-manifest.json 文件放在项目根目录下

如果你没有使用 laravel,你的 mix-manifest.json 文件会被放到项目根目录下,如果你不喜欢的话,可以调用 mix.setPublicPath('dist/'),然后 manifest 文件就会被放到 dist 目录下。

怎样使用 webpack 自动加载模块

webpack 使用 ProvidePlugin 插件加载一些需要的模块,常用的一个例子就是加载 jQuery:

1
2
3
4
5
6
7
8
9
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
});

// in a module
$('#item'); // <= just works
jQuery('#item'); // <= just works
// $ is automatically set to the exports of module "jquery"

当 laravel-mix 自动加载模块的时候(像上面说的那样),你如果想禁用(传一个空对象)或者用你自己的模块覆盖它,可以调用 mix.autoload() 方法:

1
2
3
4
mix.autoload({
jquery: ['$', 'window.jQuery', 'jQuery'], // more than one
moment: 'moment' // only one
});

为什么我看到一个 “Vue packages version mismatch”错误

如果, 更新你的依赖, 你有以下编译失败的信息

1
2
3
4
5
6
Module build failed: Error:

Vue packages version mismatch:

* vue@2.5.13
* vue-template-compiler@2.5.15

这意味着你的 vuevue-template-compiler 依赖不同步, 每一个 Vue 的更新, 版本号必须是相同的. 更新来修复这个错误

1
2
3
4
5
npm update vue

// or

npm install vue@2.5.15

排障

我在更新/安装 mix 时候出现错误

不幸的是,你的依赖项可能没有正确安装的原因有无数个。一个常见的根本原因是安装了老版本的 Node(node -v) 和 npm (npm -v)。第一步,访问 http://nodejs.org 并更新它们。

否则,它通常与需要删除的错误锁文件有关。让这一系列命令尝试从头开始安装一切

1
2
3
4
rm -rf node_modules
rm package-lock.json yarn.lock
npm cache clear --force
npm install

为什么 webpack 不能找到我的 app.js 条目文件?

如果你遇到这样的失败信息……

1
2
3
These dependencies were not found:

* /Users/you/Sites/folder/resources/assets/js/app.js

… 你可能使用 npm 5.2 (npm -v) 这个版本。这个版本引入了一个导致安装错误的错误。该问题已被在 npm 5.3 修复。请升级,然后重新安装

1
2
3
4
rm -rf node_modules
rm package-lock.json yarn.lock
npm cache clear --force
npm install

API

Javascript

1
mix.js(src|[src], output)

简单的一行代码,larave mix 可以执行很多重要的操作

  • ES2017+ 模块编译
  • 创建并且编译 vue 组件(通过 vue-loader)
  • 模块热替换(HMR)
  • Tree-shaking 打包技术,webpack2 里新增的(移除无用的库)
  • 提取和拆分 vendor 库(通过mix.extract()方法), 使长期缓存变的容易
  • 自动版本化(文件哈希),通过 mix.version()

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let mix = require('laravel-mix');

// 1. A single src and output path.
mix.js('src/app.js', 'dist/app.js');


// 2. For additional src files that should be
// bundled together:
mix.js([
'src/app.js',
'src/another.js'
], 'dist/app.js');


// 3. For multiple entry/output points:
mix.js('src/app.js', 'dist/')
.js('src/forum.js', 'dist/');

laravel 示例

考虑到典型的 laravel 默认安装的时候会把入口定位在 ./resources/assets/js/app.js,所以我们先准备一个 webpack.mix.jsapp.js 编译到 ./public/js/app.js

1
2
3
let mix = require('laravel-mix');

mix.js('resources/assets/js/app.js', 'public/js');

现在上面所有的项你都可以用了,只需要调用一个方法。

在命令行调用 npm run dev 执行编译。

Vue 组件

laravel mix 包罗万象,支持 vue 组件编译,如果你不使用 vue 的话,可以忽略这块。

单文件组件是 vue 最重要的特征。在一个文件里为一个组件定义模板,脚本,样式表。

./resources/assets/js/app.js

1
2
3
4
5
6
7
import Vue from "vue";
import Notification from "./components/Notification.vue";

new Vue({
el: "#app",
components: { Notification },
});

在上面,我们导入了 vue(首先你需要执行npm install vue --save-dev安装 vue),然后引入了一个叫 Notification 的 vue 组件并且注册了 root vue 实例。

./resources/asset/js/components/Notification.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="notification">
{{ body }}
</div>
</template>

<script>
export default {
data() {
return {
body: 'I am a notification.'
}
}
}
</script>
<style>
.notification {
background: grey;
}
</style>

如果你了解 vue,这些你都会很熟悉,继续。

./webpack.mix.js

1
2
3
let mix = require('laravel-mix');

mix.js('resources/assets/js/app.js', 'public/js');

执行 npm run dev 编译文件,这样就简单的创建了一个 HTML 文件,引入 ./js/app.js 文件,然后在浏览器里查看吧!

React 支持

laravel mix 也装载了基本的 react 支持,只要把 mix.js()改成 mix.react()并且使用相同的参数。在底层,mix 会引用 react 需要的任何 babel 插件。

1
mix.react('resources/assets/js/app.jsx', 'public/js/app.js');

当然,你仍然需要使用 npm 安装 react 和 reactDOM,不过要注意小心行事。

库代码分离

1
2
mix.js(src, output)
.extract(['any', 'vendor', 'library']);

把所有的 js 都打包成一个文件会伴随着潜在的风险:每次更新项目中就算很小的一部分都需要破坏所有用户的缓存,这意味着你的第三方库需要重新被下载和缓存。这样很不好。

一个解决的办法是分离或者提取你的库文件。

  • 应用代码: app.js
  • vendor 库: vendor.js
  • Manifest(webpack runtime): manifest.js
1
mix.extract(['vue', 'jquery']);

extract 方法接受一个你想要从打包文件里提取出的库的数组,使用这个方法,Vue 和 jQuery 的源代码都会被放在 vendor.js 里。如果在未来你需要对应用代码做一些微小的变动,并不会对大的 vendor 库产生影响,它们依然会留在长期缓存。

一旦执行 webpack 打包文件,你会发现三个新的文件,你可以在 HTML 页面引用它们。

1
2
3
<script src="/js/manifest.js"></script>
<script src="/js/vendor.js"></script>
<script src="/js/app.js"></script>

实际上,我们付出了一些 HTTP 请求的代价(就是会多请求几次)来换取长期缓存的提升。

Manifest 文件是什么

webpack 编译的时候会有一些 run-time 代码协助其工作。如果没有使用 mix.extract(),这些代码你是看不到的,它会在打包文件里,然而,如果我们分离了代码并且允许长期缓存,在某些地方就需要这些 run-time 代码,所以,mix 会把它提取出来,这样一来,vendor 库和 manifest 文件都会被缓存很长时间。

浏览器自动刷新

1
mix.browserSync('my-site.test');

BrowserSync 能自动监控文件变动并且把你的变化通知浏览器, – 完全不需要手动刷新。你可以调用 mix.browserSync() 方法来开启这个功能:

1
2
3
4
5
6
7
8
mix.browserSync('my-domain.test');

// Or:

// https://browsersync.io/docs/options/
mix.browserSync({
proxy: 'my-domain.test'
})

参数可以传字符串(proxy),也可以传对象(BrowserSync 设置)。你声明的域名作为 proxy 是非常重要的,Browsersync 将通过代理网址来输出到你的虚拟主机(webpack dev server).

其他选项可以从 Browsersync Documentation

现在, 启动 dev server (npm run watch), 并进行下一步操作吧.

模块热替换

laravel mix 对模块热替换提供了无缝的支持。

模块热替换(或者叫热加载),意思就是当 javascript 改变刷新页面的时候可以维持组件的状态,例如现在有一个计数器,按一下按钮,计数器会加 1,想象一下你点了很多次然后修改一下组件的相关文件,浏览器会实时的反映出你所做出的更改而保持计数器不变,计数器不会被重置,这就是热加载的意义最在。

在 laravel 里的用法

Laravel 和 Laravel 一起工作, 来抽象出热加载的复杂性.

看一下 laravel 里的 package.json 文件,在 scripts 模块,你可以看到:

1
2
3
4
5
6
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --progress --hide-modules",
"watch": "cross-env NODE_ENV=development webpack --watch --progress --hide-modules",
"hot": "cross-env NODE_ENV=development webpack-dev-server --inline --hot",
"production": "cross-env NODE_ENV=production webpack --progress --hide-modules"
}

注意一下 hot 选项,这个地方就是你所需要的,在命令行执行 npm run hot 会启动一个 node 服务器并且监视你的 bundle 文件,接下来,在浏览器打开你的 laravel 应用,一般应该是 http://my-app.test

在 laravel 应用里使用热加载很重要的一点是要保证所有的脚本资源引用的是前面启动的 node 服务器的 url:http://localhost:8080,现在你可以手动更新你的 HTML\Blade 文件了:

1
2
3
4
<body>
<div id="app">...</div>
<script src="http://localhost:8080/js/bundle.js"></script>
</body>

假设你有一些组件,尝试在浏览器里更改他们的状态,然后更新他们的模板文件,你可以看到浏览器会立刻反应出你的更新,但是状态并没有被改变。

但是,在开发部署环境下手动更新 url 会是一个负担,所以,laravel 提供了一个 mix()方法,他会动态的构建 js 或者样式表的引用,然后输出。上面的代码因此可以修改成:

1
2
3
4
5
<body>
<div id="app"></div>

<script src="{{ mix('js/bundle.js') }}"></script>
</body>

调整之后,Laravel 将为你做这项工作。如果运行 npm run hot 以启用热重加载,则该函数将设置必要的 http://localhost:8080 作为 URL。相反,如果您使用 npm run devnpm run pro,它将使用域名作为基准 url。

在 Https 中使用
如果你在 HTTPS 连接上开发你的应用,你的热重加载脚本和样式也必须通过 HTTPS 服务。要实现这一点,可以将 -—https 标志添加到 package.json 中的热选项命令中。

1
2
3
"scripts": {
"hot": "NODE_ENV=development webpack-dev-server --inline --hot --https",
}

通过上面的设置,webpack-dev-server 将会生成一个自签名证书。如果你希望使用自己的证书,可以使用以下设置:

1
"hot": "NODE_ENV=development webpack-dev-server --inline --hot --https --key /path/to/server.key --cert /path/to/server.crt --cacert /path/to/ca.pem",

现在在你的 Html/Blade 文件中可以使用

1
<script src="https://localhost:8080/js/bundle.js"></script>

或者

1
<script src="{{ mix('js/bundle.js') }}"></script>

在 spa 里的用法

laravel mix 包含了流行的 vue-loader 包,这意味着,如果是单页应用,你什么都不需要做,它是开箱即用的。

版本化

1
2
mix.js('src', 'output')
.version([]);

为了帮助长期缓存,Laravel Mix 提供了 mix.version() 方法,它支持文件散列。比如app.js?id=8e5c48eadbfdd5458ec6。这对清除缓存很有用。假设你的服务器自动缓存了一年的脚本,以提高性能。这很好,但是,每当您对应用程序代码进行更改时,需要一些方法来告诉用户更新缓存, 这通常是通过使用查询字符串或文件哈希来完成的。

启用了版本控制之后,每次代码更改时,都会生成一个新的散列查询字符串文件。看以下webpack.mix.js 文件

编译后,你会在 mix-manifest.json 文件看到 /css/app.css?id=5ee7141a759a5fb7377a/js/app.js?id=0441ad4f65d54589aea5。当然,你的特定散列将是唯一的。每当你调整 JavaScript 时,编译后的文件将会收到一个新的散列名称,这将有效地破坏缓存,一旦被推到生产环境中。

举个例子,试试 webpack --watch,然后修改一下你的 JavaScript。你将立即看到一个新生成的打包文件和样式表。

导入版本文件

这就引出了一个问题:如果名称不断变化,我们如何将这些版本化的脚本和样式表包含到 HTML 中呢?是的,这很棘手。答案将取决于你构建的应用程序的类型。对于 SPA,你可以动态地读取 Laravel Mix 生成的 manifest.json 文件,提取资料文件名(这些名称将被更新,以反映新的版本文件),然后生成 HTML。

Laravel 用户

对于 Laravel 项目,一个解决方案是开箱即用的。只需调用全局 mix() 函数,就完成了!我们将计算出导入的适当文件名。这里有一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>App</title>
<link rel="stylesheet" href="{{ mix('css/app.css') }}" />
</head>
<body>
<div id="app">
<h1>Hello World</h1>
</div>

<script src="{{ mix('js/app.js') }}"></script>
</body>
</html>

将未散列的文件路径传递给 mix() 函数,然后在后端,我们将弄清楚应该导入哪个脚本或样式表。请注意,你可能/应该使用这个函数,即使没有对文件进行版本控制。

版本化附加文件

mix.version() 将自动生成编译后的 JavaScript、Sass/Less 或合并文件。但是,如果你还希望将额外的文件作为构建的一部分,简单地传递路径或路径数组,就像这样:

1
mix.version(['public/js/random.js']);

现在,我们会版本化任何相关的编译文件,但是我们还会附加一个查询字符串,public/js/random.js?{hash},并更新 mix-manifest.json 文件。

Css 预处理器

1
2
3
4
5
mix.sass('src', 'output', pluginOptions);
mix.standaloneSass('src', 'output', pluginOptions); // Isolated from Webpack build.
mix.less('src', 'output', pluginOptions);
mix.stylus('src', 'output', pluginOptions);
mix.postCss('src', 'output', [ require('precss')() ])

一个单一的方法调用允许你编译你的 Sass,Less,或 Stylus 文件,同时自动应用 CSS3 前缀。

虽然 webpack 可以将所有 CSS 直接插入到绑定的 JavaScript 中,但 Laravel Mix 会自动执行必要的步骤,将其提取到你想要的输出路径中。

多构建

如果你需要编译多个顶级文件,你可以根据需要调用 mix.sass()(或任何一个预处理器变体). 对于每个调用,webpack 将输出一个包含相关内容的新文件。

1
2
mix.sass('src/app.scss', 'dist/') // creates 'dist/app.css'
.sass('src/forum.scss', 'dist/'); // creates 'dist/forum.css'

例子

让我们看一个例子

webpack.mix.js

1
2
3
let mix = require('laravel-mix');

mix.sass('resources/assets/sass/app.sass', 'public/css');

./resources/assets/sass/app.sass

1
2
3
4
$primary: grey

.app
background: $primary

Tip : 对于 Sass 编译, 你可以使用 .sass.scss 语法

像往常一样运行 npm run webpack 进行编译. 你会发现 ./public/css/app.css 文件包含

1
2
3
.app {
background: grey;
}

插件选项

编译的时候, Laravel Mix 的首先是去分别的调用 Node-Sass, Less,和 Slylus 来编译你的 Sass, Less 文件。有时,你可能需要重写我们传递给它们的默认选项。可以将它们作为第三个参数提供给 mix.sass(), mix.less()mix.stylus()

Stylus 插件
如果使用 Stylus, 你可能希望安装额外的插件,比如 Rupture 。运行 npm install rupture 来安装这个插件,然后在你的 mix.stylus() 中调用, 例如:

1
2
3
4
5
mix.stylus('resources/assets/stylus/app.styl', 'public/css', {
use: [
require('rupture')()
]
});

如果希望更深一步,并且在全局中自动导入插件,您可以使用 import 选项。这里有一个例子:

1
2
3
4
5
6
7
8
9
10
11
mix.stylus('resources/assets/stylus/app.styl', 'public/css', {
use: [
require('rupture')(),
require('nib')(),
require('jeet')()
],
import: [
'~nib/index.styl',
'~jeet/jeet.styl'
]
});

就是这个样子滴!

CSS url() 重写

一个关键的 webpack 概念是,它将重写你的样式表中的任何 url()。虽然这可能最初听起来很奇怪,但它是一项非常强大的功能。

一个例子

假设我们想要编译一些 Sass,其中包含一个图像的相对 url。

1
2
3
.example {
background: url("../images/thing.png");
}

提示:url()的绝对路径将被排除在 url 重写之外。因此,url('/images/thing.png')url('http://example.com/images/thing.png') 将不会被更改。

注意,这里说的是相对 URL? 默认情况下,Laravel Mix 和 webpack 将会找到 thing.png ,将其复制到 public/images 文件夹中,然后在生成的样式表中重写 url()。因此,编译的 CSS 将是:

1
2
3
.example {
background: url(/images/thing.png?d41d8cd98f00b204e9800998ecf8427e);
}

这也是 webpack 的一个很酷的特性。然而,它确实有一种倾向,让那些不理解 webpack 和 css-loader 插件如何工作的人感到困惑。你的文件夹结构可能已经是您想要的了,而且你希望不要修改那些url()。如果是这样的话,我们确实提供了一个覆盖方式:

1
2
3
4
mix.sass('src/app.scss', 'dist/')
.options({
processCssUrls: false
});

把这个加上你的 webpack.mix.js 文件中,我们将不再匹配 url() 或复制资源到你的公共目录。因此,编译后的 CSS 将与你输入时一样:

1
2
3
.example {
background: url("../images/thing.png");
}

这样的好处是,当禁用 url 处理时,您的 Webpack, Sass 编译和提取可以更快地编译。

PostCSS 插件

默认情况下,Mix 将通过流行的 Autoprefixer PostCSS plugin 将所有的 CSS 文件连接起来。因此,你可以自由使用最新的 CSS 3 语法,并理解我们将自动应用任何必要的浏览器前缀。在大多数情况下,默认设置应该就可以,但是,如果你需要调整底层的自动修复程序配置,那么如下所示:

1
2
3
4
5
6
7
8
9
10
mix.sass('resources/assets/sass/app.scss', 'public/css')
.options({
autoprefixer: {
options: {
browsers: [
'last 6 versions',
]
}
}
});

另外,如果你想完全禁用它——或者依赖一个已经包含 自动前缀的 PostCSS 插件:

1
2
mix.sass('resources/assets/sass/app.scss', 'public/css')
.options({ autoprefixer: false });

但是,你可能想要在构建中应用额外的 PostCSS 插件。木问题, 只需在 NPM 中安装所需的插件,然后在 webpack.mix.js 文件中引用,如下所示:

1
2
3
4
5
6
mix.sass('resources/assets/sass/app.scss', 'public/css')
.options({
postCss: [
require("postcss-custom-properties")
]
});

完成了!现在可以使用和编译自定义 CSS 属性(如果这是您的东西)。例如,如果 resources/assets/sass/app.scss 包含…

1
2
3
4
5
6
7
:root {
--some-color: red;
}

.example {
color: var(--some-color);
}

编译完成将会是

1
2
3
.example {
color: red;
}

PostCss 不适用 Sass/Less

或者,如果你更喜欢跳过 Sass/Less/Stylus 编译步骤,而是使用 PostCSS,你可以通过mix.postCss() 方法来完成。

1
2
3
mix.postCss('resources/assets/css/main.css', 'public/css', [
require('precss')()
]);

请注意,第三个参数是应该应用于你构建的 postcss 插件的数组。

独立 Sass 构建

如果你不希望 Mix 和 Webpack 以任何方式处理你的 Sass 文件,你可以使用mix.standaloneSass(),这将大大改善你应用程序的构建时间。请记住:如果你选择了这条路线,Webpack 不会触及你的 CSS。它不会重写 url,复制资源(通过 file-loader),或者应用自动图像优化或 CSS 清理。如果这些特性对于你的应用程序来说是没有必要的,那么一定要使用这个选项而不是mix.sass()

1
mix.standaloneSass('resources/assets/sass/app.scss', 'public/css');

注意:如果你正在使用 standaloneSass,在使用 npm run watch 进行文件更改时,你将需要使用下划线来前缀导入的文件,以将它们标记为依赖文件(例如,_header.scss _alert.scss)。如果不这样做,将导致 Sass 编译错误和/或额外的 CSS 文件。

文件复制

1
2
3
4
mix.copy(from, to);
mix.copy('from/regex/**/*.txt', to);
mix.copy([path1, path2], to);
mix.copyDirectory(fromDir, toDir);

有时, 你需要复制一个或者多个文件作为构建过程的一部分。没有问题, 这是小事一桩。使用mix.copy() 方法指定源文件或文件夹,然后指定您想要的目标文件夹/文件

1
mix.copy('node_modules/vendor/acme.txt', 'public/js/acme.txt');

在编译时,’acme’ 文件将被复制到 ‘public/js/acme.txt’。这是一个常见的用例,当你希望将一组字体通过 NPM 安装到 public 目录时。

系统通知

默认情况下,Laravel Mix 将显示每个编译的系统通知。这样,你就可以快速查看是否有需要查询的错误。但是,在某些情况下,这是不可取的(例如在生产服务器上编译)。如果发生这种情况,它们可以从你的 webpack.mix.js 文件中禁用。

1
2
mix.js(src, output)
.disableNotifications();

文件组合和最小化

1
2
3
4
mix.combine(['src', 'files'], 'destination');
mix.babel(['src', 'files'], destination);
mix.minify('src');
mix.minify(['src']);

如果使用得当,Laravel Mix 和 webpack 应该负责所有必要的模块捆绑和最小化。但是,你可能有一些遗留代码或第三方库需要连接和最小化, 这并不是一个问题。

组合文件

考虑下面的代码片段:

1
mix.combine(['one.js', 'two.js'], 'merged.js');

这自然会合并 one.jstwo.js 到一个单独的文件,叫做 merged.js。与往常一样,在开发期间,合并文件将保持未压缩状态。但是,对于生产(export NODE_ENV=production),这个命令将会最小化 merged.js

组合文件与 Babel 编译。

如果需要组合 使用 ES2015 方法编写的 JavaScript 文件,你可以更新的 mix.combine() 调用 mix.babel()。方法签名相同。唯一的区别是,在将文件组合起来之后,Laravel Mix 将对结果进行 Babel 编译,从而将代码转换成所有浏览器都能理解的 JavaScript 代码。

1
mix.babel(['one.js', 'two.js'], 'merged.js');

最小化文件

同样,你也可以使用 mix.minify() 命令缩小一个或多个文件。

1
2
mix.minify('path/to/file.js');
mix.minify(['this/one.js', 'and/this/one.js']);

这里有一些值得注意的事情:

  • 该方法将创建一个额外的 *.min.ext 文件。因此,压缩 app.js 将生成 app.min.js
  • 再一次声明,压缩只会在生产过程中发生。(export NODE_ENV=production)。
  • 不需要调用 mix.combine(['one.js', 'two.js'], 'merged.js').minify('merged.js'); ,只使用单一的 mix.combine() 调用。它会兼顾两者。

    重要:请注意,压缩只适用于 CSS 和 JavaScript 文件。minifier 不理解任何其他提供的文件类型。

自动加载

1
2
3
mix.autoload({
jquery: ['$', 'window.jQuery']
});

Webpack 提供了必要的功能,可以在 Webpack 所要求的每个模块中把一个模块作为变量。如果你使用的是一个特定的插件或库,它依赖于一个全局变量,例如 jQuery, mix.autoload() 可能会对你有用。

考虑下面的例子:

1
2
3
mix.autoload({
jquery: ['$', 'window.jQuery']
});

该代码片段指定 webpack 应该将 var $ = require('jquery') 添加到它所遇到的全局$标识符或 window.jQuery 中。漂亮!

事件钩子

1
mix.then(function () {});

你可能需要监听每次 webpack 完成编译的事件。也许你需要手动应用一些适合你的应用程序的逻辑。如果是这样,您可以使用 mix.then() 方法来注册任何回调函数。这里有一个例子:

1
2
3
4
mix.js('resources/assets/js/app.js', 'public/js')
.then(() => {
console.log('webpack has finished building!');
});

回调函数将通过 webpack Stats 对象,允许对所执行的编译进行检查:

1
2
3
4
5
mix.js('resources/assets/js/app.js', 'public/js')
.then((stats) => {
// array of all asset paths output by webpack
console.log(Object.keys(stats.compilation.assets));
});

可以在这里找到 Stats 对象的官方文档 : https://github.com/webpack/docs/wiki/node.js-api#stats

快速 webpack 配置

1
mix.webpackConfig({} || cb);

当然,你可以自由编辑提供的 webpack.config.js 文件,在某些设置中,更容易直接从你的 webpack.mix.js 修改或覆盖默认设置。对于 Laravel 应用来说尤其如此,默认情况下是在项目根文件夹中没有 webpack.config.js

例如,你可能希望添加一个由 webpack 自动加载的模块的自定义数组。在这个场景中,您有两个选项:

  • 根据需要编辑你的 webpack.config.js 文件
  • 在你的 webpack.mix.js 中调用 mix.webpackConfig() 文件,并传递重写参数。然后混合将进行一个深度合并。
    下面,作为一个示例,我们将为 Laravel Spark 添加一个自定义模块路径。
1
2
3
4
5
6
7
8
mix.webpackConfig({
resolve: {
modules: [
'node_modules',
path.resolve(__dirname, 'vendor/laravel/spark/resources/assets/js')
]
}
});

使用回调函数

当传递回调函数时,你可以访问 webpack 及其所有属性。

1
2
3
4
5
6
7
8
9
10
11
mix.webpackConfig(webpack => {
return {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery',
})
]
};
});

扩展 Mix

基于组件的系统 Mix 使用场景构建它的 API,你也可以访问—是否为你的项目扩展 Mix,或者作为一个可重用的包分发到世界。

例子

1
2
3
4
5
6
7
8
9
// webpack.mix.js;
let mix = require('laravel-mix');

mix.extend('foo', function(webpackConfig, ...args) {
console.log(webpackConfig); // the compiled webpack configuration object.
console.log(args); // the values passed to mix.foo(); - ['some-value']
});

mix.js('src', 'output').foo('some-value');

在上面的示例中,我们可以看到 mix.extend() 接受两个参数:在定义组件时应该使用的名称,以及一个回调函数或类,这些函数注册并组织必要的 webpack 逻辑。在后台,一旦构建了底层 webpack 配置对象,Mix 将触发这个回调函数。这将给你一个机会来插入或覆盖任何必要的设置。

虽然简单的回调函数可能对快速扩展很有用,但在大多数情况下,您可能希望构建一个完整的组件类,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mix.extend(
'foo',
new class {
register(val) {
console.log('mix.foo() was called with ' + val);
}

dependencies() {}

webpackRules() {}

webpackPlugins() {}

// ...
}()
);

在扩展 Mix 时,通常需要触发一些指令:

  • 安装这些依赖关系。
  • 将此规则/加载程序添加到 webpack 中。
  • 包含这个 webpack 插件。
  • 完全覆盖 webpack 配置的这一部分。
  • 将此配置添加到 Babel。
  • 等。

这些操作中的任何一个都是带有 Mix 组件系统有联系。

组件的接口

  • name:当调用组件时,应该使用什么作为方法名。(默认为类名。)
  • dependencies:列出应该由 Mix 安装的所有 npm 依赖项。
  • register:当您的组件被调用时,所有的用户参数将立即被传递给这个方法。
  • boot:启动组件。这个方法是在用户的 webpack.mix 之后触发的。js 文件已经加载完毕。
  • webpackEntry:附加到主混合 webpack 入口对象。
  • webpackRules:与主 webpack 加载器合并的规则。
  • webpackplugin:与主 webpack 配置合并的插件。
  • webpackConfig:覆盖生成的 webpack 配置。
  • babelConfig:额外的 Babel 配置应该与 Mix 的默认值合并。

这里有一个示例/虚拟组件,它可以让你更好地了解如何构建自己的组件。更多的例子,请参考在后台 Mix 使用的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
class Example {
/**
* The optional name to be used when called by Mix.
* Defaults to the class name, lowercased.
*
* Ex: mix.example();
*
* @return {String|Array}
*/
name() {
// Example:
// return 'example';
// return ['example', 'alias'];
}

/**
* All dependencies that should be installed by Mix.
*
* @return {Array}
*/
dependencies() {
// Example:
// return ['typeScript', 'ts'];
}

/**
* Register the component.
*
* When your component is called, all user parameters
* will be passed to this method.
*
* Ex: register(src, output) {}
* Ex: mix.yourPlugin('src/path', 'output/path');
*
* @param {*} ...params
* @return {void}
*
*/
register() {
// Example:
// this.config = { proxy: arg };
}

/**
* Boot the component. This method is triggered after the
* user's webpack.mix.js file has executed.
*/
boot() {
// Example:
// if (Config.options.foo) {}
}

/**
* Append to the master Mix webpack entry object.
*
* @param {Entry} entry
* @return {void}
*/
webpackEntry(entry) {
// Example:
// entry.add('foo', 'bar');
}

/**
* Rules to be merged with the master webpack loaders.
*
* @return {Array|Object}
*/
webpackRules() {
// Example:
// return {
// test: /\.less$/,
// loaders: ['...']
// });
}

/*
* Plugins to be merged with the master webpack config.
*
* @return {Array|Object}
*/
webpackPlugins() {
// Example:
// return new webpack.ProvidePlugin(this.aliases);
}

/**
* Override the generated webpack configuration.
*
* @param {Object} webpackConfig
* @return {void}
*/
webpackConfig(webpackConfig) {
// Example:
// webpackConfig.resolve.extensions.push('.ts', '.tsx');
}

/**
* Babel config to be merged with Mix's defaults.
*
* @return {Object}
*/
babelConfig() {
// Example:
// return { presets: ['react'] };
}

请注意,上面示例中的每个方法都是可选的。在某些情况下,您的组件可能只需要添加一个 webpack 加载程序和/或调整混合使用的 Babel 配置。没有问题的话省略其余的接口。

1
2
3
4
5
6
7
8
class Example {
webpackRules() {
return {
test: /\.test$/,
loaders: []
};
}
}

现在,当 Mix 构造底层 webpack 配置时,你的规则将包含在生成的webpackConfig.module.rules 数组中。

使用

一旦你构建或安装了想要的组件,只需从 webpack.mix.js 中获取它即可,你都准备好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// foo-component.js

let mix = require('laravel-mix');

class Example {
webpackRules() {
return {
test: /\.test$/,
loaders: []
};
}
}

mix.extend('foo', new Example());
1
2
3
4
5
6
7
8
9
// webpack.mix.js

let mix = require('laravel-mix');
require('./foo-component');

mix
.js('src', 'output')
.sass('src', 'output')
.foo();

自定义方法

LiveReload

当 Laravel Mix 与 Browsersync 已经支持了开箱即用,你可能更喜欢使用 LiveReload, 当检测到修改时,LiveReload 可以自动监视您的文件并刷新页面。

安装 webpack-livereload-plugin

1
npm install webpack-livereload-plugin@1 --save-dev

配置 webpack.mix.js

将以下几行添加到 webpack.mix.js 底部。

1
2
3
4
5
6
7
var LiveReloadPlugin = require('webpack-livereload-plugin');

mix.webpackConfig({
plugins: [
new LiveReloadPlugin()
]
});

虽然 LiveReload 有她很好用的默认值,但是这里可以查看一个可用的插件选项列表

安装 LiveReload.js

最后,我们需要安装 LiveReload.js。您可以通过 LiveReload Chrome 插件,或者在你的主要站点模板的关闭 </body> 标记之前添加以下代码:

1
2
3
@if(config('app.env') == 'local')
<script src="http://localhost:35729/livereload.js"></script>
@endif

运行 dev server

1
npm run watch

现在,LiveReload 将自动监控您的文件并在必要时刷新页面。享受吧!

Jquery UI

jQuery UI 是一个用于呈现公共组件的工具包,比如 datepickers、draggables 等。不需要做任何调整,以使其与 Laravel Mix 一起工作。

构建 webpack.mix.js 配置

1
2
mix.js('resources/assets/js/app.js', 'public/js')
.sass('resources/assets/sass/app.scss', 'public/css');

安装 jquery-ui

1
npm install jquery-ui --save-dev

加载必要插件

1
2
3
4
5
6
// resources/assets/js/app.js

import $ from 'jquery';
window.$ = window.jQuery = $;

import 'jquery-ui/ui/widgets/datepicker.js';

加载 CSS

1
2
3
// resources/assets/sass/app.scss

@import '~jquery-ui/themes/base/all.css';

触发 UI 组件

1
2
// resources/assets/js/app.js
$('#datepicker').datepicker();

高级配置

Laravel Mix 配置项

1
2
3
4
5
6
7
8
9
10
mix.options({
extractVueStyles: false,
processCssUrls: true,
uglify: {},
purifyCss: false,
//purifyCss: {},
postCss: [require('autoprefixer')],
clearConsole: false
});

如果需要的话可以使用一些混合选项和覆盖选项。请注意上面的选项,以及它们的默认值。这里有一个快速概述:

  • extractVueStyles:提取 .vue 组件样式(CSS 在<style>标签内)到一个专用文件,而不是将其嵌入到 HTML 中。
  • globalVueStyles:表示一个文件包含在每个组件样式中。这个文件应该只包含变量、函数或 mixin,以便在最终的编译文件中防止重复的 css。这个选项只有在启用了提取工具时才有效。
  • processCssUrls:进程/优化相对样式表url()。默认情况下,Webpack 会自动更新这些 url。但是,如果您的文件夹结构已经按照您想要的方式进行组织,那么将此选项设置为 false 以禁用处理。
  • uglify:使用这个选项来合并你的项目需要的任何定制的 uglify 选项。
  • purifyCss:如果你想要混合自动读取你的 HTML/Blade 文件,并删除你的 CSS 包,你可以将这个选项设置为 true。您还可以传递包含 purifycss-webpack 选项的对象。
  • postCss:合并任何自定义的 postCss 插件。
  • clearConsole:设置为 false,如果您不想在每次构建后清除终端/控制台。

Faq

Laravel 错误 Class log does not exist …

Fatal error: Uncaught exception ‘ReflectionException’ with message ‘Class log does not exist’ in /Users/freek/dev/laravel/vendor/laravel/framework/src/Illuminate/Container/Container.php:776

出现这种问题的原因是不能够加载 log 方法. 原因是在加载的时候会加载 config 文件的数据, 而 config 文件中的配置是批量加载的, 所以在自己加载的时候 config 文件的写法不支持自定义的函数变量/ 常量/ 自定义方法.

所以从配置文件入手, 删除未加载的配置文件, 删除未导入包的配置文件.

这种问题一般出现在 复制项目, 并且删除了包的情况下.

参考文章:

[Plugin] 第三方包

排名 扩展包 一句话描述
1 intervention/image 图片处理扩展包,支持裁剪、水印等处理,使用教程请见  https://phphub.org/topics/1903
2 barryvdh/laravel-debugbar 页面调试工具栏 (对 phpdebugbar 的封装),教程请见:https://phphub.org/topics/2531
3 barryvdh/laravel-ide-helper 使用 IDE 开发 Laravel 项目的好帮手,支持 Facade 方法跳转,相关讨论请见:https://phphub.org/topics/2532
4 maatwebsite/excel Excel 处理工具,中文处理时会出现乱码,推荐使用  laravel-snappy,历史讨论请见  https://phphub.org/topics/2477
5 aws/aws-sdk-php-laravel 亚马逊 AWS 服务的开发者工具包,亚马逊云已经在 2016 年 8 月  正式落地中国,这个包以后会常用到,教程请见:https://phphub.org/topics/2533
6 jenssegers/agent 客户端 User Agent 解析工具(基于 Mobiledetect),教程请见:https://phphub.org/topics/782
7 bugsnag/bugsnag-laravel Bugsnag 服务集成包(异常捕获服务,可惜国内访问效果不好),教程请见:https://phphub.org/topics/2534
8 zizaco/entrust 基于用户组的用户权限系统(必备),教程请见:https://phphub.org/topics/166
9 barryvdh/laravel-cors 跨域资源共享的支持
10 barryvdh/laravel-dompdf PDF 操作工具(基于 dompdf )
11 laravelbook/ardent 自动  数据模型  验证工具
12 tymon/jwt-auth JWT (JSON Web Token) 用户认证机制,示例项目  https://phphub.org/topics/2023
13 lucadegasperi/oauth2-server-laravel OAuth 2.0 支持,实例教程:https://phphub.org/topics/1792
14 maknz/slack Slack 服务的集成
15 jenssegers/mongodb MongoDB 数据库的支持 ,教程:https://phphub.org/topics/309
16 dingo/api 构建 API 服务器的完整解决方案,教程:https://phphub.org/topics/1159
17 itsgoingd/clockwork 配合 Chrome 浏览器下同名插件的调试工具,教程:https://phphub.org/topics/23
18 anahkiasen/underscore-php Underscore.js 类似的 PHP 语法支持
19 laracasts/generators Laracasts 出品的代码快速生成工具(推荐) ,使用教程:https://phphub.org/topics/2535
20 cviebrock/eloquent-sluggable 文章标题 URL 别名处理工具,教程:https://phphub.org/topics/1926
21 laracasts/testdummy Laracasts 出品的假数据创建工具
22 davejamesmiller/laravel-breadcrumbs 页面面包屑工具,教程:https://phphub.org/topics/1914
23 laracasts/utilities 将 PHP 变量转换为 JavaScript 变量
24 roumen/sitemap Sitemap 生成工具
25 yajra/laravel-datatables-oracle jQuery DataTables 的后端支持
26 webpatser/laravel-uuid RFC 4122 标准生成的 UUID ,使用教程  https://phphub.org/topics/2538
27 rcrowe/twigbridge Twig 模板引擎支持
28 intervention/imagecache 图片缓存增强工具
29 indatus/dispatcher 计划任务分发器(直接可替换掉 Cron),L5 内置了类似的功能
30 jenssegers/date 日期处理工具(让 Carbon 支持多语言,中文用户的福音)
31 rap2hpoutre/laravel-log-viewer 非常方便的页面 Log 查看工具,必备,不过使用时请注意访问权限控制
32 baum/baum 嵌套集合 (Nested Set) 模型的支持,教程:https://phphub.org/topics/2124
33 anahkiasen/rocketeer 现代化的服务器代码部署工具
34 anahkiasen/former 强大的表单构造器,教程请见  https://phphub.org/topics/2539
35 barryvdh/laravel-snappy HTML 生成 PDF/Image 工具(利用 wkhtmltopdf)
36 thujohn/twitter Twitter API 的支持
37 orchestra/testbench Laravel 扩展包的单元测试工具
38 graham-campbell/flysystem 文件系统操作,多平台支持(AWS,Dropbox 等)
39 mews/purifier 用户提交的 Html 白名单过滤,https://phphub.org/topics/36
40 laracasts/presenter Laracasts 出品的 Presenter 方案
41 venturecraft/revisionable 数据模型的操作记录(如管理员操作日记)
42 mcamara/laravel-localization Laravel 本地化功能增强
43 league/factory-muffin 允许更加方便的创建对象,一般在测试中常用(基本上是 ROR 的 factory_girl 的复制版)
44 robclancy/presenter Elequent 的 Presenter 方案
45 intouch/laravel-newrelic 应用状态监控服务 NewRelic 开发者工具包
46 xethron/migrations-generator 从现存的数据中以 migration 的形式导出数据库表,包括索引和外键,相当于  数据库迁移
47 greggilbert/recaptcha reCAPTCHA 验证码的支持
48 watson/validating 以 Trait 的方式来实现 Eloquent 数据模型保存的时候自动验证
49 dimsav/laravel-translatable 数据库的多语言翻译方案
50 laracasts/behat-laravel-extension Behat 测试框架的 Laravel 支持
51 jenssegers/rollbar Rollbar 错误监控服务的自动集成
52 torann/geoip 通过 IP 获取到对应的地理位置信息(GeoIP 数据库),请参考:https://phphub.org/topics/2537
53 davibennun/laravel-push-notification App 的 Push Notification 发送工具,支持苹果的 APNS 和 安卓的 GCM
54 chumper/zipper ZIp 打包工具(基于 ZipArchive)
55 simplesoftwareio/simple-qrcode 二维码生成工具
56 graham-campbell/markdown Markdown 解析器
57 aloha/twilio Twillio API 支持
58 propaganistas/laravel-phone 手机号码,电话号码验证支持
59 orangehill/iseed 将数据从数据库以 seed 的方式导出,数据填充  的逆向操作。(推荐)
60 sammyk/laravel-facebook-sdk (非官方)Laravel 的 Facebook 开发者工具包
61 vinkla/hashids Hash ID 生成器,方便把数字的 ID 隐藏(基于 Hashids),教程:https://phphub.org/topics/2536
62 spatie/laravel-backup 数据备份工具,支持压缩,支持各种文件系统(推荐)
63 mccool/laravel-auto-presenter 自动注入 Presenter,教程:https://phphub.org/topics/1267
64 graham-campbell/throttle 阀门控制工具
65 frozennode/administrator 快速创建基于数据模型的 CRUD 管理员后台,教程:https://phphub.org/topics/158https://phphub.org/topics/2407
66 codesleeve/laravel-stapler 专为 ORM 定制的文件上传支持
67 webpatser/laravel-countries 世界所有国家数据,包括首都汇率等
68 prettus/l5-repository Repository 开发模式的支持
69 pragmarx/google2fa 用户认证方案,支持谷歌提倡的双向认证和 HOTP 认证算法
70 hisorange/browser-detect 浏览器检测工具,包括客户端对 JavaScript 和 CSS 支持情况的检测,教程:https://phphub.org/topics/2046
71 graham-campbell/htmlmin 基于 minify 的 HTML 压缩工具
72 toin0u/geocoder-laravel 地理位置操作工具集(基于 Geocoder)
73 edvinaskrucas/notification 页面消息提醒的组件
74 laracasts/integrated PHPUnit 的集成测试支持
75 laravel/envoy Laravel 官方出品的简单的部署工具,教程:https://phphub.org/topics/24
76 felixkiss/uniquewith-validator 表单验证规则增加字段之间的唯一性验证
77 graham-campbell/exceptions 错误异常处理工具,支持开发和生产环境,使用 Whoops 进行错误显示
78 thomaswelton/laravel-gravatar Gravatar 服务的支持
79 mews/captcha 图片验证码方案
80 roumen/feed Feed 生成器
81 cviebrock/image-validator 表单验证增加图片专属,如长宽,比例等
82 laravelcollective/annotations 基于注解方式生成路由、事件、模型绑定的映射
83 gloudemans/shoppingcart 一个简单的购物车模块实现
84 artisaninweb/laravel-soap Soap 协议客户端
85 jlapp/swaggervel Swagger API 规范支持
86 barryvdh/laravel-translation-manager 翻译辅助工具,包含 Web 界面
87 patricktalmadge/bootstrapper Twitter Bootstrap 支持
88 soapbox/laravel-formatter 对不同输出格式进行转换,支持 Array,CSV,JSON,XML,YAML
89 fedeisas/laravel-mail-css-inliner 将 CSS 样式写入 HTML 里,用于邮件发送内容的样式定制
90 nicolaslopezj/searchable 以 Trait 的形式为 Eloquent 模型增加搜索功能
91 benconstable/phpspec-laravel PHPSpec BDD 测试框架的 Laravel 扩展
92 watson/rememberable 让 Laravel 5 数据模型支持 remember() 方法
93 rtconner/laravel-tagging 为 Eloquent 模型增加打标签功能
94 laravelcollective/remote LaravelCollective 维护的 SSH 连接管理工具
95 khill/lavacharts Google 图表 JavaScript API 的封装
96 anchu/ftp 让 Laravel 支持 FTP 操作
97 liebig/cron 计划任务分发器(直接可替换掉 Cron),L5 内置了类似的功能
98 lord/laroute JavaScript 读取路由信息的解决方案
99 spatie/laravel-analytics Google 统计数据获取工具
100 hieu-le/active 非常方便的方案来判断导航元素的  active  状态

[转+] Laravel API 结合 Dingo API 和 JWT

原文地址: Laravel API结合Dingo API和JWT

介绍

关于API的开发 不得不提的就是可以利用Dingo来构建更加强大的API 这样我们可以更好的去实现API认证和请求;
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

安装插件

安装 dinggo Api

首先当然是去安装页面 根据提供的包进行下载 在laravel项目中就是require这个package, 然后 composer update -vvv

1
"dingo/api": "1.0.*@dev"

接着在laravel项目的configapp.php去添加 ServiceProvider

1
2
3
'providers' => [
Dingo\Api\Provider\LaravelServiceProvider::class
]

再去生成相应的配置文件

1
$ php artisan vendor:publish --provider="Dingo\Api\Provider\LaravelServiceProvider"

安装 Jwt

如果需要实现jwt 同样的也是去安装页面 安, 配置 composer.json 文件, 然后运行 composer update -vvv
在整理这篇文章的时候, 1.0 已经在 rc 版本, 快要发布了. 而 0.5 版本中有些不兼容的问题, 故而这里采用的是 1.0 版本, 引入的 ServiceProvider 有所不同, 在安装以及设置上需要注意

1
"tymon/jwt-auth": "1.0.*@dev",

添加对应的服务:

1
Tymon\JWTAuth\Providers\LaravelServiceProvider::class

配置的 alias

1
2
'JWTAuth'    => Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,

生成配置文件

1
$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

生成 jwt key

1
$ php artisan jwt:secret

使用

配置 Dinggo Api

这个时候我们是在开发的环境下 还需对Dingo进行相应的配置 在.env文件里

1
2
3
4
5
6
7
8
9
10
11
# 类型
API_STANDARDS_TREE=vnd

# 前缀
API_PREFIX=api

# 版本
API_VERSION=v1

# 开启 Debug 模式
API_DEBUG=true

配置认证

我们可以实现一个 jwtauth 认证 在 config/api.php 里配置

1
2
3
4
5
6
7
8
'auth' => [
'basic'=>function($app){
return new Dingo\Api\Auth\Provider\Basic($app['auth']);
},
'jwt'=>function($app){
return new Dingo\Api\Auth\Provider\JWT($app['Tymon\JWTAuth\JWTAuth']);
}
],

这样我们就实现了在Dingojwt认证

注册中间件

既然是auth认证我们就需要先注册刚配置好的认证 即在Kernel文件里添加

1
2
'jwt.auth'        => 'Tymon\JWTAuth\Http\Middleware\Check',
'jwt.refresh' => 'Tymon\JWTAuth\Http\Middleware\RefreshToken',

添加路由

laravel 5.2以后的版本我们可以直接放在 routes/api.php

1
$api = app('Dingo\Api\Routing\Router');

为了区分 我们可以在app目录下新建Api目录, 然后再新建Controllers和在Http目录一样 在这里用来管理api的控制器

在这个目录下新建一个基本的控制器 BaseController

1
2
3
4
5
6
7
8
<?php
namespace App\Api\Controllers;
use App\Http\Controllers\Controller;
use Dingo\Api\Routing\Helpers;
class BaseController extends Controller
{
use Helpers;
}

此时我们再去创建对数据的api时就可以继承这个控制器并可以使用 Dingo ApiHelpers 函数.

比如在此目录下创建PostsController

这样我们就可以在 routes.php 里根据 Dingo 提供的方法去定义api

1
2
3
4
5
6
7
8
9
10
11
$api = app('Dingo\Api\Routing\Router');

$api->version('v1', function ($api) {
$api->group([
'namespace' => 'App\Api\Controllers'
], function ($api) {
$api->get('lessons','PostsController@index');
$api->get('lessons/{id}','PostsController@show');
});
});

PostsControllerindex返回所有数据 那么再去访问http://localhost:8000/api/lessons 就可以看到所有的数据了

在这里使用 dinggo 的方法路由, 这和框架定义路由方式不太一样

当然和之前的一样 我们需要对数据字段进行映射 那么我们可以在Api目录下新建Transformer目录 然后在这个目录下新建PostTransformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
namespace App\Api\Transformer;
use App\Post;
use League\Fractal\TransformerAbstract;
class PostTransformer extends TransformerAbstract
{
public function transform(Post $post)
{
return [
'title' => $post['title'],
'content' => $post['body'],
'is_free' => (boolean)$ppost['free']
];
}
}

在这里我们是可以使用Dingo APITransformerTransformerAbstract

这样写完我们就可以在控制器里去重新返回所有信息

1
2
3
4
5
 public function index()
{
$lessons = Post::all();
return $this->collection($post,new PostTransformer());
}

这里的PostTransformer是 App\Api\Transformer\PostTransformer

当然还有之前的show方法 因为他的返回状态信息之前都是自己写的 其实在Dingo里也有相应的方法

1
2
3
4
5
6
7
8
public function show($id)
{
$lesson = Lesson::find($id);
if(! $lesson){
return $this->response->errorNotFound('Lesson not found');
}
return $this->item($lesson,new LessonTransformer());
}

结合Jwt的auth认证

Auth 认证的前提

User 模型需要实现 Tymon\JWTAuth\Contracts\JWTSubject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use Tymon\JWTAuth\Contracts\JWTSubject as JWTSubjectAuthenticatable;

class User extends Model implements JWTSubjectAuthenticatable
{
use Authenticatable;

...

/**
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey(); // Eloquent model method
}

/**
* @return array
*/
public function getJWTCustomClaims()
{
return [
'user' => [
'id' => $this->id,
...
]
];
}
}

Jwt 认证

App\Api\Controllers目录下新建AuthController并继承之前定义好的BaseController

jwt创建token的页面 我们就可以使用它的authenticate方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function authenticate(Request $request)
{
// grab credentials from the request
$credentials = $request->only('email','password');
try {
// attempt to verify the credentials and create a token for the user
if (! $token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch (JWTException $e) {
// something went wrong whilst attempting to encode the token
return response()->json(['error' => 'could_not_create_token'], 500);
}
// all good so return the token
return response()->json(compact('token'));
}

为了执行这个方法 可以去路由中定义

1
2
3
4
5
6
$api->version('v1', function ($api) {
$api->group(['namespace' => 'App\Api\Controllers'], function ($api) {
$api->post('user/login','AuthController@authenticate');
$api->post('user/register','AuthController@register');
});
});

这个时候再去查看一下我们的路由的话就会看到新定义的post路由

为了验证请求的结果 我们可以使用postman这个chrome工具 去请求http://localhost:8000/api/user/login

这个时候是会返回{"error":"invalid_credentials"}

为了能够正确通过我们可以在body部分给出用户邮箱和密码(用户可用thinker创建一个) 这个时候就会正确返回一个token

这个token就是用来保护有jwt认证下的信息

添加 jwt 认证限制

我们可以为Post的数据添加一个middleware

1
2
3
4
$api->group(['middleware'=>'jwt.auth'],function ($api){
$api->get('posts',PostsController@index');
$api->get('posts/{id}','PostsController@show');
});

所以这个时候如果我们没有之前authenticate返回的token的话 我们是无法访问api/postsapi/post/{id}

只有加上返回的token我们才能继续访问到之前的数据信息 如/api/posts?token=xxxxxx

既然只有登录的用户才能访问到这些资源 那么我们是不是也可以去拿到登录的用户

jwtAuthentication里就提供了getAuthenticatedUser这个方法 所以为了查看效果 可以去注册一条路由

1
2
3
$api->group(['middleware'=>'jwt.auth'],function ($api){
$api->get('user/me','AuthController@getAuthenticatedUser');
});

接着在AuthController里去定义这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function getAuthenticatedUser()
{
try {
if (! $user = JWTAuth::parseToken()->authenticate()) {
return response()->json(['user_not_found'], 404);
}
} catch (TokenExpiredException $e) {
return response()->json(['token_expired'], $e->getStatusCode());
} catch (TokenInvalidException $e) {
return response()->json(['token_invalid'], $e->getStatusCode());
} catch (JWTException $e) {
return response()->json(['token_absent'], $e->getStatusCode());
}
// the token is valid and we have found the user via the sub claim
return response()->json(compact('user'));
}

所以说这时候去访问http://localhost:8000/api/user/me?token=xxx就可以拿到当前登录的用户信息了

相关链接

laravel 生命周期

laravel 的生命周期主要分为 3 个主要阶段:

  • 加载项目依赖,
  • 创建应用实例,
  • 接受请求并响应

入口文件实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
// 一
require __DIR__ . '/../vendor/autoload.php';

//二
$app = require_once __DIR__ . '/../bootstrap/app.php';

//三
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

生命周期

1.加载项目依赖

php 依赖于 composer 包管理,通过引入由 composer 包管理器自动生成的类加载程序,进行注册并加载第三方组件.

1
2
<?php
require __DIR__ . '/../vendor/autoload.php';

2.创建应用实例

1
2
创建应用实例/服务容器,执行代码位于bootstrap/app.php文件,创建应用实例过程包括:注册项目基础服务、注册项目服务提供者别名、注册目录路径等等一系列操作
如下为app.php的代码,主要完成创建应用实例和绑定核心内容到服务容器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
*/

$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);

/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
*/

$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);

$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);

$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);

/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
*/

return $app;

2.1 创建应用实例

即对 Illuminate\Foundation\Application 进行实例化,称之为 app 容器。在实例化 app 容器的时候,完成以下几个工作:

  • 注册应用基础路径并绑定到 app 服务容器;
  • 注册基础服务提供者至 app 服务容器;
  • 注册核心容器别名到 app 服务容器;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Create a new Illuminate application instance.
*
* @param string|null $basePath
* @return void
*/
public function __construct($basePath = null)
{
if ($basePath) {
$this->setBasePath($basePath);
}

$this->registerBaseBindings();

$this->registerBaseServiceProviders();

$this->registerCoreContainerAliases();
}

2.2 绑定内核

1
2
3
laravel根据http请求的环境,将请求分别发送至相应的http内核(Http\Kernel::class)和console内核(Console\Kernel::class),
无论 HTTP内核还是 Console内核,它们都是接收一个 HTTP请求,随后返回一个响应。
在http内核中定义了【中间件】相关数组,在Illuminate\Foundation\Http\Kernel类中,定义了属性 $bootstrappers的引导程序数组
  • 中间件 : 提供了一种方便的机制来过滤进入应用的 HTTP 请求。
  • 引导程序:包括完成环境检测、配置加载、异常处理、Facades 注册、服务提供者注册、启动服务这六个引导程序。
1
2
3
4
5
6
7
8
protected $bootstrappers = [
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];

2.3 注册异常处理

3.接收请求并响应

1
2
3
4
5
6
7
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);

$response->send();

3.1 解析内核程序

第二阶段已经将 http 内核和 console 内核绑定到了 app 服务容器,使用 app 服务容器的 make 方法,将内核解析出来

1
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
  • 内核实例化的时候又进行了哪些操作呢,进一步查看 Illuminate\Foundation\Http\Kernel 内核的
    __construct(Illuminate\Contracts\Foundation\Application $app, \Illuminate\Routing\Router $router)构造方法,
  • 它接收 APP 容器和路由器两个参数:
    在实例化内核时,构造函数内将在 HTTP 内核定义的「中间件组」注册到 路由器,注册完后就可以在实际处理 HTTP 请求前
    调用这些「中间件」实现 过滤 请求的目的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
/**
* Create a new HTTP kernel instance. 创建 HTTP 内核实例
*
* @class Illuminate\Foundation\Http\Kernel
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function __construct(Application $app, Router $router)
{
$this->app = $app;
$this->router = $router;

$router->middlewarePriority = $this->middlewarePriority;

foreach ($this->middlewareGroups as $key => $middleware) {
$router->middlewareGroup($key, $middleware);
}

foreach ($this->routeMiddleware as $key => $middleware) {
$router->aliasMiddleware($key, $middleware);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
/**
* Register a group of middleware. 注册中间件组
*
* @class \Illuminate\Routing\Router
* @param string $name
* @param array $middleware
* @return $this
*/
public function middlewareGroup($name, array $middleware)
{
$this->middlewareGroups[$name] = $middleware;

return $this;
}

/**
* Register a short-hand name for a middleware. 注册中间件别名
*
* @class \Illuminate\Routing\Router
* @param string $name
* @param string $class
* @return $this
*/
public function aliasMiddleware($name, $class)
{
$this->middleware[$name] = $class;

return $this;
}

3.2 处理 http 请求

1
之前的所有处理,基本都是围绕在配置变量、注册服务等运行环境的构建上,构建完成后才开始处理一个「HTTP 请求」。

处理请求包含两个阶段:

  • 创建请求实例
  • 处理请求
1
2
3
4
5
6
<?php
// 处理请求
$response = $kernel->handle(
// 创建请求实例
$request = Illuminate\Http\Request::capture()
);

3.2.1 创建请求实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* Create a new Illuminate HTTP request from server variables.
*
* @class Illuminate\Http\Request
* @return static
*/
public static function capture()
{
static::enableHttpMethodParameterOverride();
return static::createFromBase(SymfonyRequest::createFromGlobals());
}

/**
* Create an Illuminate request from a Symfony instance.
*
* @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/Request.php
* @param \Symfony\Component\HttpFoundation\Request $request
* @return \Illuminate\Http\Request
*/
public static function createFromBase(SymfonyRequest $request)
{
if ($request instanceof static) {
return $request;
}

$content = $request->content;

$request = (new static)->duplicate(
$request->query->all(), $request->request->all(), $request->attributes->all(),
$request->cookies->all(), $request->files->all(), $request->server->all()
);

$request->content = $content;

$request->request = $request->getInputSource();

return $request;
}

3.2.2 处理请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Handle an incoming HTTP request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();

$response = $this->sendRequestThroughRouter($request);
} catch (Exception $e) {
$this->reportException($e);

$response = $this->renderException($request, $e);
} catch (Throwable $e) {
$this->reportException($e = new FatalThrowableError($e));

$response = $this->renderException($request, $e);
}

$this->app['events']->dispatch(
new Events\RequestHandled($request, $response)
);

return $response;
}

handle() 方法接收一个 HTTP 请求,并最终生成一个 HTTP 响应。

继续深入到处理 HTTP 请求的方法 内部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Send the given request through the middleware / router.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);

Facade::clearResolvedInstance('request');

$this->bootstrap();

return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}

这段代码完成了如下操作:

  • 将 $request 实例注册到 APP 容器 供后续使用;

  • 清除之前 $request 实例缓存;

  • 启动「引导程序」;

  • 发送请求至路由。

    3.2.2.1 启动引导程序

上述$this->bootstrap();进行了引导程序的启动,调用了 app 容器的 bootstrapWith();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* The bootstrap classes for the application. 应用的引导程序
*
* @var array
*/
protected $bootstrappers = [
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];

/**
* Bootstrap the application for HTTP requests.
*
* @class Illuminate\Foundation\Http\Kernel
* @return void
*/
public function bootstrap()
{
if (! $this->app->hasBeenBootstrapped()) {
$this->app->bootstrapWith($this->bootstrappers());
}
}

/**
* Get the bootstrap classes for the application.
*
* @return array
*/
protected function bootstrappers()
{
return $this->bootstrappers;
}

/**
* Get the bootstrap classes for the application.
*
* @return array
*/
protected function bootstrappers()
{
return $this->bootstrappers;
}

看一下 Illuminate\Foundation\Application 的 bootstrapWith()方法是如何来启动这些引导程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Run the given array of bootstrap classes.
*
* @class Illuminate\Foundation\Application
* @param array $bootstrappers
* @return void
*/
public function bootstrapWith(array $bootstrappers)
{
$this->hasBeenBootstrapped = true;

foreach ($bootstrappers as $bootstrapper) {
$this['events']->fire('bootstrapping: '.$bootstrapper, [$this]);

$this->make($bootstrapper)->bootstrap($this);

$this['events']->fire('bootstrapped: '.$bootstrapper, [$this]);
}
}

我们看到在 APP 容器内,会先解析对应的「引导程序」,随后调用「引导程序」的 bootstrap() 完成的「引导程序」的启动操作。

  • 引导程序列表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
protected $bootstrappers = [
/*
|--------------------------------------------------------------------------
| 环境检测,将.env配置信息载入到$_ENV变量中
|--------------------------------------------------------------------------
|Detect if a custom environment file matching the APP_ENV exists,
*/
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,

/*
|--------------------------------------------------------------------------
| 加载配置文件
|--------------------------------------------------------------------------
|Load the configuration items from all of the files,
*/
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,

/*
|--------------------------------------------------------------------------
| 异常处理
|--------------------------------------------------------------------------
|Convert PHP errors to ErrorException instances
*/
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,

/*
|--------------------------------------------------------------------------
| 注册Facades
|--------------------------------------------------------------------------
|注册完成后可以以别名的方式访问具体的类
*/
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,

/*
|--------------------------------------------------------------------------
| 注册服务提供者
|--------------------------------------------------------------------------
|在「创建应用实例」已经将基础服务提供者注册到APP容器。在这里会将配置在 app.php文件夹下
|providers节点的服务器提供者注册到 APP 容器,供请求处理阶段使用;
*/
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,

//启动服务
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];

我们选取 Illuminate\Foundation\Bootstrap\LoadConfiguration::class,来查看一下启动的原理,它的功能是加载配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
$items = [];

if (file_exists($cached = $app->getCachedConfigPath())) {
$items = require $cached;

$loadedFromCache = true;
}

$app->instance('config', $config = new Repository($items));

if (! isset($loadedFromCache)) {
$this->loadConfigurationFiles($app, $config);
}

$app->detectEnvironment(function () use ($config) {
return $config->get('app.env', 'production');
});

date_default_timezone_set($config->get('app.timezone', 'UTC'));

mb_internal_encoding('UTF-8');
}

/**
* Load the configuration items from all of the files.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Contracts\Config\Repository $repository
* @return void
* @throws \Exception
*/
protected function loadConfigurationFiles(Application $app, RepositoryContract $repository)
{
$files = $this->getConfigurationFiles($app);

if (! isset($files['app'])) {
throw new Exception('Unable to load the "app" configuration file.');
}

foreach ($files as $key => $path) {
$repository->set($key, require $path);
}
}

「创建 Laravel 应用实例」的时候执行了一步「注册应用的基础路径并将路径绑定到 APP 容器」的操作。
当前,LoadConfiguration 类就是将 config 目录下的所有配置文件读取到一个集合中,这样我们就可以使用 config()辅助函数获取配置数据

  • 以下为 config()助手函数的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (! function_exists('config')) {
/**
* Get / set the specified configuration value.
*
* If an array is passed as the key, we will assume you want to set an array of values.
*
* @param array|string $key
* @param mixed $default
* @return mixed|\Illuminate\Config\Repository
*/
function config($key = null, $default = null)
{
if (is_null($key)) {
return app('config');
}

if (is_array($key)) {
return app('config')->set($key);
}

return app('config')->get($key, $default);
}
}

3.2.2.2 发送请求至路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Send the given request through the middleware / router.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);

Facade::clearResolvedInstance('request');

$this->bootstrap();

return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}

在 「发送请求至路由」这行代码中,完成了四个不同的操作:

  • 管道(pipeline)创建
  • 将 $request 传入管道
  • 对 $request 执行「中间件」处理
  • 实际的请求处理
    先来看看$this->dispatchToRouter() 这个方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Get the route dispatcher callback. 获取一个路由分发器匿名函数
*
* @return \Closure
*/
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);

return $this->router->dispatch($request);
};
}

在进行「解析内核实例」的时候已经将 Illuminate\Routing\Router 对象赋值给$this->router 属性;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<?php
class Router implements RegistrarContract, BindingRegistrar
{
use Macroable {
__call as macroCall;
}

/**
* Dispatch the request to the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function dispatch(Request $request)
{
$this->currentRequest = $request;

return $this->dispatchToRoute($request);
}

/**
* Dispatch the request to a route and return the response. 将请求分发到路由并返回响应
*
* @param \Illuminate\Http\Request $request
* @return mixed
*/
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}

/**
* Find the route matching a given request. 查找对应的路由实例
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Routing\Route
*/
protected function findRoute($request)
{
$this->current = $route = $this->routes->match($request);

$this->container->instance(Route::class, $route);

return $route;
}

/**
* Return the response for the given route. 运行给定的路由
*
* @param Route $route
* @param Request $request
* @return mixed
*/
protected function runRoute(Request $request, Route $route)
{
$request->setRouteResolver(function () use ($route) {
return $route;
});

$this->events->dispatch(new Events\RouteMatched($route, $request));

return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}

/**
* Run the given route within a Stack "onion" instance. 通过一个实例栈运行给定的路由
*
* @param \Illuminate\Routing\Route $route
* @param \Illuminate\Http\Request $request
* @return mixed
*/
protected function runRouteWithinStack(Route $route, Request $request)
{
$shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
$this->container->make('middleware.disable') === true;

$middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);

return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run()
);
});
}
}

执行$route->run()的方法定义在 Illuminate\Routing\Route 类中,最终执行在「routes/web.php 配置的匹配到的控制器或匿名函数」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Run the route action and return the response. 执行路由方法并返回响应
*
* @return mixed
*/
public function run()
{
$this->container = $this->container ?: new Container;

try {
if ($this->isControllerAction()) {
return $this->runController();
}

return $this->runCallable();
} catch (HttpResponseException $e) {
return $e->getResponse();
}
}

其执行结果会通过 Illuminate\Routing\Router::prepareResponse($request, $response)生一个响应实例并返回。
至此,Laravel 就完成了一个 HTTP 请求的请求处理。

4.发送响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
// 一
require __DIR__ . '/../vendor/autoload.php';

//二
$app = require_once __DIR__ . '/../bootstrap/app.php';

//三
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

发送响应由 Illuminate\Http\Response 父类 Symfony\Component\HttpFoundation\Response 中的 send() 方法完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Sends HTTP headers and content.
*
* @return $this
*/
public function send()
{
//发送响应头
$this->sendHeaders();

//发送报文主题
$this->sendContent();

if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif (!\in_array(PHP_SAPI, array('cli', 'phpdbg'), true)) {
static::closeOutputBuffers(0, true);
}

return $this;
}

5.程序终止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* Call the terminate method on any terminable middleware.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Response $response
* @return void
*/
public function terminate($request, $response)
{
$this->terminateMiddleware($request, $response);

$this->app->terminate();
}

/**
* Call the terminate method on any terminable middleware.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Response $response
* @return void
*/
protected function terminateMiddleware($request, $response)
{
$middlewares = $this->app->shouldSkipMiddleware() ? [] : array_merge(
$this->gatherRouteMiddleware($request),
$this->middleware
);

foreach ($middlewares as $middleware) {
if (! is_string($middleware)) {
continue;
}

list($name) = $this->parseMiddleware($middleware);

$instance = $this->app->make($name);

if (method_exists($instance, 'terminate')) {
$instance->terminate($request, $response);
}
}
}

以上便是 Laravel 的请求生命周期的始末。

[Plugin] Simple Captcha 包 (developer-tz/laravel-5-simple-captcha)

一个简单的 Laravel 5 包是 Simple Captcha for Laravel 5.

安装

这个 Simple Captcha 服务提供者通过 Composer 安装 ,在 composer.json 中的 require 分支中配置

developer-tz/simple-captcha 包并且设置 minimum-stabilitydev .

1
2
3
4
5
6
7
{
"require": {
"laravel/framework": "5.*",
"developer-tz/simple-captcha": "dev-master"
},
"minimum-stability": "dev"
}

运行  composer update 或者 composer install.

Windows 平台下, 你需要配置 php 支持  php_gd2.dll.

Usage / 使用

使用 Simple Captcha 服务, 你需要在 Laravel 启动时候注册服务.

这里有两种方式来设置.

config/app.php 找到 providers 部分, 按照如下注册服务提供者.

1
2
3
4
'providers' => array(
// ...
'DeveloperTz\SimpleCaptcha\SimpleCaptchaServiceProvider',
)

找到 config/app.php 的  aliases key .

1
2
3
4
'aliases' => array(
// ...
'SimpleCaptcha' => 'DeveloperTz\SimpleCaptcha\Facades\SimpleCaptcha',
)

Configuration / 配置

使用自己的配置需要首先发布下.

To use your own settings, publish config.

1
$ php artisan vendor:publish

Example Usage / 示例

译注: 不起作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// [your site path]/app/routes.php
Route::any('/simple-captcha-test', function()
{

if (Request::getMethod() == 'POST')
{
$rules = array('captcha' => array('required', 'captcha'));
$validator = Validator::make(Input::all(), $rules);
if ($validator->fails())
{
echo '<p style="color: #ff0000;">Incorrect!</p>';
}
else
{
echo '<p style="color: #00ff30;">Matched :)</p>';
}
}

$content = Form::open(array(URL::to(Request::segment(1))));
$content .= '<p>' . HTML::image(SimpleCaptcha::img(), 'Captcha image') . '</p>';
$content .= '<p>' . Form::text('captcha') . '</p>';
$content .= '<p>' . Form::submit('Check') . '</p>';
$content .= '<p>' . Form::close() . '</p>';
return $content;

});

^_^

原生 php 代码写在 laravel 框架中代码不生效

问题来源: 使用 laravel 框架直接在 php 中输出图片是不显示的.
源码是:
直接输出

1
2
3
4
5
6
7
8
Route::get('png', function () {
$im = @imagecreate(200, 50) or die("创建图像资源失败");
imagecolorallocate($im, 255, 255, 255);
$text_color = imagecolorallocate($im, 0, 0, 255);
imagestring($im, 5, 0, 0, "Hello world!", $text_color);
imagepng($im);
imagedestroy($im);
});

显示内容如下:

这里的头信息是:

需要继续修改头信息, 于是更改为如下的代码

存在头信息

1
2
3
4
5
6
7
8
9
10
11
12
13
Route::get('png', function () {
ob_start();
$im = @imagecreate(200, 50) or die("创建图像资源失败");
imagecolorallocate($im, 255, 255, 255);
$text_color = imagecolorallocate($im, 0, 0, 255);
imagestring($im, 5, 0, 0, "Hello world!", $text_color);
imagepng($im);
imagedestroy($im);
$content = ob_get_clean();
return response($content, 200, [
'Content-Type' => 'image/png',
]);
});

有些框架可以, 但是有些框架也不行:
可以的显示:

不可以的显示:

在不可以的框架中, 由于考虑可能是缓存/缓冲区的问题, 输出下 ob_get_status() 结果发现是有内容的

1
2
3
4
5
6
7
8
9
10
Array
(
[name] => default output handler
[type] => 0
[flags] => 112
[level] => 0
[chunk_size] => 4096
[buffer_size] => 8192
[buffer_used] => 1
)

所以我们需要清除缓冲区内容, 然后重新生成并且输出, 也就是输出的时候是保证不要输出任何内容/ 包含空行, 当然出现这个问题的原因可能是编码的时候是 utf-8 但是存在 bom 头导致的信息.

最终源码解决如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Route::get('png', function () {
if (ob_get_status()) {
ob_end_clean();
}
ob_start();
$im = @imagecreate(200, 50) or die("创建图像资源失败");
imagecolorallocate($im, 255, 255, 255);
$text_color = imagecolorallocate($im, 0, 0, 255);
imagestring($im, 5, 0, 0, "Hello world!", $text_color);
imagepng($im);
imagedestroy($im);
$content = ob_get_clean();
return response($content, 200, [
'Content-Type' => 'image/png',
]);
});

问题解决:

Laravel-Excel 3.0 文档

基本用法

最简单的导出方法是创建一个自定义的导出类, 这里我们使用发票导出作为示例.

App/Exports 下创建一个 InvoicesExport

1
2
3
4
5
6
7
8
9
10
11
namespace App\Exports;

use Maatwebsite\Excel\Concerns\FromCollection;

class InvoicesExport implements FromCollection
{
public function collection()
{
return Invoice::all();
}
}

在控制器中你可以使用如下方式来下载

1
2
3
4
public function export() 
{
return Excel::download(new InvoicesExport, 'invoices.xlsx');
}

或者存储在 s3 磁盘中

1
2
3
4
public function storeExcel() 
{
return Excel::store(new InvoicesExport, 'invoices.xlsx', 's3');
}

依赖注入

如果你的导出需要依赖, 你可以注入导出类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace App\Exports;

use Maatwebsite\Excel\Concerns\FromCollection;

class InvoicesExport implements FromCollection
{
public function __construct(InvoicesRepository $invoices)
{
$this->invoices = $invoices;
}

public function collection()
{
return $this->invoices->all();
}
}
1
2
3
4
public function export(Excel $excel, InvoicesExport $export) 
{
return $excel->download($export, 'invoices.xlsx');
}

严格的 null 对比

如果你希望 0 在 excel 单元格中就是显示 0, 而不是显示 null(空单元格), 你可以使用 WithStrictNullComparison

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace App\Exports;

use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithStrictNullComparison;

class InvoicesExport implements FromCollection, WithStrictNullComparison
{
public function __construct(InvoicesRepository $invoices)
{
$this->invoices = $invoices;
}

public function collection()
{
return $this->invoices->all();
}
}

Collection 全局定义/宏

这个包提供了 laravel collection 的一些额外的方法(宏) 来简单的下载或者是存储到 excel

把 collection 作为 Excel 下载

1
2
3
4
5
(new Collection([[1, 2, 3], [1, 2, 3]]))->downloadExcel(
$filePath,
$writerType = null,
$headings = false
)

在磁盘上存储 collection

1
2
3
4
5
6
(new Collection([[1, 2, 3], [1, 2, 3]]))->storeExcel(
$filePath,
$disk = null,
$writerType = null,
$headings = false
)

在磁盘上存储导出

导出可以存储到任何 Laravel 支持的 文件系统 中

1
2
3
4
5
6
7
8
9
10
11
public function storeExcel() 
{
// Store on default disk
Excel::store(new InvoicesExport(2018), 'invoices.xlsx');

// Store on a different disk (e.g. s3)
Excel::store(new InvoicesExport(2018), 'invoices.xlsx', 's3');

// Store on a different disk with a defined writer type.
Excel::store(new InvoicesExport(2018), 'invoices.xlsx', 's3', Excel::XLSX);
}

Exportables / 可导出的

在之前的例子中, 我们使用 Excel::download 这个 facade 来开始一个导出.

Laravel-Excel 同样支持  Maatwebsite\Excel\Concerns\Exportable trait, 来让一个类可以直接导出, 当然, 这个类里边需要有 collection 方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace App\Exports;

use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\Exportable;

class InvoicesExport implements FromCollection
{
use Exportable;

public function collection()
{
return Invoice::all();
}
}

我们可以不通过 facade 直接进行类的下载

1
return (new InvoicesExport)->download('invoices.xlsx');

或者是存储到磁盘上.

1
return (new InvoicesExport)->store('invoices.xlsx', 's3');

Responsable / 可响应的

之前的例子可以做的简单一点, 例如我们添加 Laravel 的 Responsable 到导出类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace App\Exports;

use Illuminate\Contracts\Support\Responsable;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\Exportable;

class InvoicesExport implements FromCollection, Responsable
{
use Exportable;

/**
* It's required to define the fileName within
* the export class when making use of Responsable.
*/
private $fileName = 'invoices.xlsx';

public function collection()
{
return Invoice::all();
}
}

你可以更简单的返回导出类,但是不需要调用 ->download() 方法.

1
return new InvoicesExport();

From Query / 从查询输出

在之前的例子中, 我们在导出类中进行查询, 当然这个解决方案可以用在小的导出类中. 对于更大一点数据的导出类可能造成比较大的性能开销.

通过使用 FromQuery 关系, 我们可以通过预查询一个导出, 这个场景实现的原理是查询可以分块执行.

InvoicesExport 类中,添加 FromQuery 关系, 并且添加一个查询, 并且确保不要使用 ->get() 来获取到数据!.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace App\Exports;

use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\Exportable;

class InvoicesExport implements FromQuery
{
use Exportable;

public function query()
{
return Invoice::query();
}
}

我们可以通过同样的方式来下载

1
return (new InvoicesExport)->download('invoices.xlsx');

自定义查询

这种方式可以通过自定义的参数来进行查询. 简单的作为依赖项传入导出类即可.

作为构造器惨呼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace App\Exports;

use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\Exportable;

class InvoicesExport implements FromQuery
{
use Exportable;

public function __construct(int $year)
{
$this->year = $year;
}

public function query()
{
return Invoice::query()->whereYear('created_at', $this->year);
}
}

$year 参数可以传递给导出类.

1
return (new InvoicesExport(2018))->download('invoices.xlsx');

作为设置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace App\Exports;

use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\Exportable;

class InvoicesExport implements FromQuery
{
use Exportable;

public function forYear(int $year)
{
$this->year = $year;

return $this;
}

public function query()
{
return Invoice::query()->whereYear('created_at', $this->year);
}
}

我们可以通过 forYear 方法来调整年份.

1
return (new InvoicesExport)->forYear(2018)->download('invoices.xlsx');

通过视图

我们可以通过 blade 视图来创建导出. 通过使用 FromView 关系.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace App\Exports;

use Illuminate\Contracts\View\View;
use Maatwebsite\Excel\Concerns\FromView;

class InvoicesExport implements FromView
{
public function view(): View
{
return view('exports.invoices', [
'invoices' => Invoice::all()
]);
}
}

这种方式会导出一个 Html 表格到 Excel 单元表, 例如 users.blade.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
@foreach($users as $user)
<tr>
<td>{{ $user->name }}</td>
<td>{{ $user->email }}</td>
</tr>
@endforeach
</tbody>
</table>

队列

如果你处理更大数据量的数据, 很明智的方法就是使用队列来运行.

例如下边的导出类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace App\Exports;

use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromQuery;

class InvoicesExport implements FromQuery
{
use Exportable;

public function query()
{
return Invoice::query();
}
}

我们只需要调用一个 ->queue() 方法即可.

1
return (new InvoicesExport)->queue('invoices.xlsx');

后台处理这些查询的方式是通过多任务/多切割的方式来进行. 这些任务使用正确的顺序来执行. 并且保证之前的查询都是正确的.

另一种方式的队列实现

你可以将导出作为一个可以扔到队列中的实现(利用 Laravel), 可以使用 ShouldQueue 约束.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace App\Exports;

use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromQuery;
use Illuminate\Contracts\Queue\ShouldQueue;

class InvoicesExport implements FromQuery, ShouldQueue
{
use Exportable;

public function query()
{
return Invoice::query();
}
}

在控制器中可以调用普通的 ->store() 方法. 基于 ShouldQueue, 通过 laravel 的队列方式来实现队列处理. [ps:你需要首先自行配置队列]

1
return (new InvoicesExport)->store('invoices.xlsx');

追加任务 / jobs

queue() 方法返回一个 Laravel 的  PendingDispatch 实例, 这意味着你可以把其他的任务串联起来, 仅仅当前一个任务执行成功的时候, 后续的任务才能够被执行.

1
2
3
return (new InvoicesExport)->queue('invoices.xlsx')->chain([
new NotifyUserOfCompletedExport(request()->user()),
]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;

class InvoiceExportCompletedJob implements ShouldQueue
{
use Queueable;

public function handle()
{
// Do something.
}
}

自定义队列

PendingDispatch 返回的时候, 我们可以改变我们使用的队列.

1
return (new InvoicesExport)->queue('invoices.xlsx')->allOnQueue('exports');

多单元表

为了能够让导出支持多单元表, 需要使用 WithMultipleSheets 关系来实现. 这个 sheets() 方法需要返回一个单元表数组.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
namespace App\Exports;

use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;

class InvoicesExport implements WithMultipleSheets
{
use Exportable;

protected $year;

public function __construct(int $year)
{
$this->year = $year;
}

/**
* @return array
*/
public function sheets(): array
{
$sheets = [];

for ($month = 1; $month <= 12; $month++) {
$sheets[] = new InvoicesPerMonthSheet($this->year, $month);
}

return $sheets;
}
}

这个 InvoicesPerMonthSheet 可以实现多种关系. 例如 FromQueryFromCollection, …

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
namespace App\Exports;

use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithTitle;

class InvoicesPerMonthSheet implements FromQuery, WithTitle
{
private $month;
private $year;

public function __construct(int $year, int $month)
{
$this->month = $month;
$this->year = $year;
}

/**
* @return Builder
*/
public function query()
{
return Invoice
::query()
->whereYear('created_at', $this->year)
->whereMonth('created_at', $this->month);
}

/**
* @return string
*/
public function title(): string
{
return 'Month ' . $this->month;
}
}

以下可以下载 2018 年的所有的发票, 它包含 12 单元表来显示每个月的数据.

1
2
3
4
public function download() 
{
return (new InvoicesExport(2018))->download('invoices.xlsx');
}

数据遍历

遍历行

通过添加  WithMapping, 你可以遍历添加到单元行中的每一条数据然后并返回.
这种方法你可以控制每一列的数据, 假设你使用 Eloquent 的 query builder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithMapping;

class InvoicesExport implements FromQuery, WithMapping
{
/**
* @var Invoice $invoice
*/
public function map($invoice): array
{
return [
$invoice->invoice_number,
Date::dateTimeToExcel($invoice->created_at),
];
}
}

添加表头

可以通过添加一个  WithHeadings 约束来实现. 表头会添加到所有数据的第一行的位置上.

1
2
3
4
5
6
7
8
9
10
11
12
13
14

use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithHeadings;

class InvoicesExport implements FromQuery, WithHeadings

public function headings(): array
{
return [
'#',
'Date',
];
}
}

格式化列

你可以格式化整列, 通过添加 WithColumnFormatting, 如果你想更多范围的自定义. 推荐使用 AfterSheet 事件来直接和地城的 Worksheet 类进行交互.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace App\Exports;

use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
use Maatwebsite\Excel\Concerns\WithMapping;

class InvoicesExport implements WithColumnFormatting, WithMapping
{
public function map($invoice): array
{
return [
$invoice->invoice_number,
Date::dateTimeToExcel($invoice->created_at),
$invoice->total
];
}

/**
* @return array
*/
public function columnFormats(): array
{
return [
'B' => NumberFormat::FORMAT_DATE_DDMMYYYY,
'C' => NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE,
];
}
}

日期

当操作日期的时候. 推荐使用 \PhpOffice\PhpSpreadsheet\Shared\Date::dateTimeToExcel() 来正确的解析你的日期数据.

导出关系

Interface Explanation
Maatwebsite\Excel\Concerns\FromCollection Use a Laravel Collection to populate the export.
Maatwebsite\Excel\Concerns\FromQuery Use an Eloquent query to populate the export.
Maatwebsite\Excel\Concerns\FromView Use a (Blade) view to to populate the export.
Maatwebsite\Excel\Concerns\WithTitle Set the Workbook or Worksheet title.
Maatwebsite\Excel\Concerns\WithHeadings Prepend a heading row.
Maatwebsite\Excel\Concerns\WithMapping Format the row before it’s written to the file.
Maatwebsite\Excel\Concerns\WithColumnFormatting Format certain columns.
Maatwebsite\Excel\Concerns\WithMultipleSheets Enable multi-sheet support. Each sheet can have its own concerns (except this one).
Maatwebsite\Excel\Concerns\ShouldAutoSize Auto-size the columns in the worksheet.
Maatwebsite\Excel\Concerns\WithStrictNullComparison Uses strict comparisions when testing cells for null value.
Maatwebsite\Excel\Concerns\WithEvents Register events to hook into the PhpSpreadsheet process.

Traits

Trait Explanation
Maatwebsite\Excel\Concerns\Exportable Add download/store abilities right on the export class itself.
Maatwebsite\Excel\Concerns\RegistersEventListeners Auto-register the available event listeners.

扩展

事件

导出过程有一些事件,你可以利用这些事件与底层类进行交互,以向导出添加自定义行为。

通过使用事件,您可以连接到父包。如果你需要完全控制导出,则不需要使用诸如 “query” 或者 “view” 之类的便利方法。

事件将通过添加 WithEvents 关注来激活。在 registerEvents 方法中,你必须返回一系列事件。Key 是事件的完全限定名(FQN),Value 是可调用的事件监听器。这可以是一个闭包、可调用的数组 或 invokable 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
namespace App\Exports;

use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\BeforeExport;
use Maatwebsite\Excel\Events\BeforeWriting;
use Maatwebsite\Excel\Events\BeforeSheet;

class InvoicesExport implements WithEvents
{
/**
* @return array
*/
public function registerEvents(): array
{
return [
// Handle by a closure.
BeforeExport::class => function(BeforeExport $event) {
$event->writer->getProperties()->setCreator('Patrick');
},

// Array callable, refering to a static method.
BeforeWriting::class => [self::class, 'beforeWriting'],

// Using a class with an __invoke method.
BeforeSheet::class => new BeforeSheetHandler()
];
}

public static function beforeWriting(BeforeWriting $event)
{
//
}
}

请注意,使用 Closure 将不可能与队列导出合并,因为PHP不能序列化闭包。在这些情况下,最好使用 RegistersEventListeners 特性。

自动注册事件监听器

通过使用 RegistersEventListeners trait ,你可以自动注册事件监听器,而不需要使用 registerEvents 。只有在创建方法时,侦听器才会被注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
namespace App\Exports;

use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\RegistersEventListeners;
use Maatwebsite\Excel\Events\BeforeExport;
use Maatwebsite\Excel\Events\BeforeWriting;
use Maatwebsite\Excel\Events\BeforeSheet;
use Maatwebsite\Excel\Events\AfterSheet;

class InvoicesExport implements WithEvents
{
use Exportable, RegistersEventListeners;

public static function beforeExport(BeforeExport $event)
{
//
}

public static function beforeWriting(BeforeWriting $event)
{
//
}

public static function beforeSheet(BeforeSheet $event)
{
//
}

public static function afterSheet(AfterSheet $event)
{
//
}
}

可用的事件

Event name Payload Explanation
Maatwebsite\Excel\Events\BeforeExport $event->writer : Writer Event gets raised at the start of the process.
Maatwebsite\Excel\Events\BeforeWriting $event->writer : Writer Event gets raised before the download/store starts.
Maatwebsite\Excel\Events\BeforeSheet $event->sheet : Sheet Event gets raised just after the sheet is created.
Maatwebsite\Excel\Events\AfterSheet $event->sheet : Sheet Event gets raised at the end of the sheet process.

WriterSheet 都是可以进行宏操作的,这意味着它可以很容易地扩展以满足你的需要。Writer 和 Sheet都有一个 ->getDelegate() 方法,它返回底层的PhpSpreadsheet 类。这将允许你为 PhpSpreadsheets 方法添加快捷方法,而这个方法在这个包中是不可用的。

Writer / 写入

1
2
3
4
5
use \Maatwebsite\Excel\Writer;

Writer::macro('setCreator', function (Writer $writer, string $creator) {
$writer->getDelegate()->getProperties()->setCreator($creator);
});

Sheet / 单元表

1
2
3
4
5
use \Maatwebsite\Excel\Sheet;

Sheet::macro('setOrientation', function (Sheet $sheet, $orientation) {
$sheet->getDelegate()->getPageSetup()->setOrientation($orientation);
});

你还可以为样式单元添加一些快捷方法。你可以自由使用这个宏,或者创造你自己的语法!

1
2
3
4
5
use \Maatwebsite\Excel\Sheet;

Sheet::macro('styleCells', function (Sheet $sheet, string $cellRange, array style) {
$sheet->getDelegate()->getStyle($cellRange)->applyFromArray($style);
});

以上例子可作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
namespace App\Exports;

use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\BeforeExport;
use Maatwebsite\Excel\Events\AfterSheet;

class InvoicesExport implements WithEvents
{
/**
* @return array
*/
public function registerEvents(): array
{
return [
BeforeExport::class => function(BeforeExport $event) {
$event->writer->setCreator('Patrick');
},
AfterSheet::class => function(AfterSheet $event) {
$event->sheet->setOrientation(\PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_LANDSCAPE);

$event->sheet->styleCells(
'B2:G8',
[
'borders' => [
'outline' => [
'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THICK,
'color' => ['argb' => 'FFFF0000'],
],
]
]
);
},
];
}
}

对于 PhpSpreadsheet 方法, 可查看文档: https://phpspreadsheet.readthedocs.io/

测试 / Testing

The Excel facade can be used to swap the exporter to a fake.

测试下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @test
*/
public function user_can_download_invoices_export()
{
Excel::fake();

$this->actingAs($this->givenUser())
->get('/invoices/download/xlsx');

Excel::assertDownloaded('filename.xlsx', function(InvoicesExport $export) {
// Assert that the correct export is downloaded.
return $export->collection()->contains('#2018-01');
});
}

测试存储导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @test
*/
public function user_can_store_invoices_export()
{
Excel::fake();

$this->actingAs($this->givenUser())
->get('/invoices/store/xlsx');

Excel::assertStored('filename.xlsx', 'diskName');

Excel::assertStored('filename.xlsx', 'diskName', function(InvoicesExport $export) {
return true;
});

// When passing the callback as 2nd param, the disk will be the default disk.
Excel::assertStored('filename.xlsx', function(InvoicesExport $export) {
return true;
});
}

测试队列导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @test
*/
public function user_can_queue_invoices_export()
{
Excel::fake();

$this->actingAs($this->givenUser())
->get('/invoices/queue/xlsx');

Excel::assertQueued('filename.xlsx', 'diskName');

Excel::assertQueued('filename.xlsx', 'diskName', function(InvoicesExport $export) {
return true;
});

// When passing the callback as 2nd param, the disk will be the default disk.
Excel::assertQueued('filename.xlsx', function(InvoicesExport $export) {
return true;
});
}

Laravel 5.5 升级到 6.0 记录

1. 可以选择缓存使用 phpredis/predis

phpredis : 指的是使用 pecl 安装的 php 扩展 redis

predis   : 指的是 github 上的 predis/predis 的包

Laravel 推荐使用 phpredis 来代替 predis。原因是 predis 包很长时间没有更新

所以要记得先安装 phpredis, 然后在 config/app.php 中去掉 Redis 别名

Mac 安装

1
2
3
4
5
# 这里需要将当前版本设置为主版本才可以, 如果不是主版本则安装会太费劲
$ brew link --force php@{version}
$ pecl install igbinary
$ pecl install redis
$ brew services restart php@7.2

其他平台

应该是直接安装即可(未测试)

项目中在考虑兼容的情况下, 使用 predis, 暂时不启用 phpredis.

2. Unable to create configured logger. Using emergency logger

在 5.6 之后已经将配置文件独立 config/logging.php, 将这个文件放置到指定目录, 然后 app.php 移除日志的配置 Logging Configuration

3. Call to undefined method Illuminate\Events\Dispatcher::fire()

在 (5.8 升级指南)(https://laravel.com/docs/5.8/upgrade) 指出,

Likelihood Of Impact: Low

1
2
deprecated and removed
Events The fire Method

使用 dispatch 方法替代 You should use the dispatch method instead.

4. Class ‘Illuminate\Support\Facades\Input’ not found

使用 Request 替代 Input

Input no longer exists. Either use the Request facade or alias that instead of Input.

5. str_contains 等 helper 函数

这些函数均需要替换成静态函数方法 Str::contains

下面是 辅助函数列表

5.1 辅助函数列表

Laravel macro 示例

在运行时候动态添加函数

1
2
3
4
5
6
7
8
9
10
11
use Illuminate\Support\Collection;

Collection::macro('someMethod', function ($arg1 = 1, $arg2 = 1) {
return $this->count() + $arg1 + $arg2;
});

$coll = new Collection([1, 2, 3]);
echo $coll->someMethod(1, 2);
// 6 = 3 + (1 + 2)
echo $coll->someMethod();
// 5 = 3 + (1 + 1)

[转+]深入探讨 Service Provider

原文地址 : 深入探討 Service Provider

Service Provider 是 Laravel 管理 Package 的核心技术

Laravel 提供了 service container 让我们方便实现 依赖注入,而service provider则是我们注册及管理 service container 的地方。

事实上 Laravel 内部所有的核心组件都是使用 service provider 统一管理,除了可以用来管理 package 外,也可以用来管理自己写的物件。

定义

As Bootstrapper

我们知道 Laravel 提供了 service container,方便我们实现SOLID依赖倒转原则,当 type hint 搭配 interface 时,需要自己下App::bind(),Laravel 才知道要载入什麽物件,但App::bind()要写在哪裡呢?Laravel 提供了service provider,专门负责App::bind()

我们可以在config/app.phpproviders看到所有的 package,事实上 Laravel 核心与其他 package 都是靠 service provider 载入。

As Organizer

Taylor 在书中一直强调 : 不要认为只有package才会使用service provider,它可以用来管理自己的service container,也就是说,若因为需求而需要垫interface时,可以把 service provider 当成Simple Factory pattern 使用,将变化封装在 service provider 内,将来需求若有变化,只要改 service provider 即可,其他使用该 interface 的程式皆不必修改。Laravel: From Apprentice To Artisan

邂逅 : 安装 Package

初学者第一次接触 service provider,应该是在安装 package 时,以安装Laravel Debugbar为例,一开始我们会使用 composer 安装 : 22 详细请参考如何使用 Laravel Debugbar?

1
$ composer require barryvdh/laravel-debugbar

接着我们会在config/app.phpproviders加入Barryvdh\Debugbar\ServiceProvider::class

config/app.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
'providers' => [

/*
* Laravel Framework Service Providers...
*/
...
Illuminate\View\ViewServiceProvider::class,
Barryvdh\Debugbar\ServiceProvider::class,

/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,

],

上半部为Laravel Framework Service Provider,载入 Laravel 预设的 package。

下半部为Application Service Provider,载入自己所使用的service container

为什麽使用 composer 安装完 package 之后,还要设定 service provider 呢?

以 Laravel Debugbar 为例,使用 composer 安装完 package 之后,只是将 package 安装在/vendor/barryvdh/laravel-debugbar目录下,此时 Laravel 还不知道有这个 package,必须在config/app.php注册该 package 所提供的 service provider,Laravel 才知道 Laravel Debugbar 的存在,并在 Laravel 启动时载入时透过 Laravel Debugbar 的 service provider 去载入 Laravel Debugbar。

建立 Service Provider

一般来说,有 3 个地方我们会自己建立 service provider :

  1. 想自己载入 package。(As Bootstrapper)
  2. 想管理自己的 service container。(As Organizer)
  3. 自己写 package。请参考 如何开发自己的 Package?

自己载入 Package

使用–dev 安装 package

以 Laravel Debugbar 为例,虽然可以使用 package 所提供的 service provider,并在config/app.php中注册,不过由于 Laravel Debugbar 属于开发用的 package,因此我不希望正式上线主机也安装,若使用之前的安装方式,则连正式上线主机也会有 Laravel Debugbar。

1
$ composer require barryvdh/laravel-debugbar --dev

composer 加上--dev参数后,package 只会安装在require-dev区段,将来在正式上线主机只要下composer install --no-dev,就不会安装 Laravel Debugbar。

composer require执行完,composer.json内容会如下图所示 :

config.json

1
2
3
4
5
6
7
8
9
10
11
12
"require": {
"php": ">=5.5.9",
"laravel/framework": "5.1.*"
},
"require-dev": {
"fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~4.0",
"phpspec/phpspec": "~2.1",
"laravel/homestead": "^2.1",
"barryvdh/laravel-debugbar": "^2.0"
},

产生 Service Provider

1
$ php artisan make:provider MyLaravelDebugbarServiceProvider

app\Providers\目录下会建立自己的MyLaravelServiceProvider.php,预设会有boot()register()

app/Providers/MyLaravelServiceProvider.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class MyLaravelDebugbarServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
//
}

/**
* Register the application services.
*
* @return void
*/
public function register()
{
//
}
}

所有的 service provider 都是继承Illuminate\Support\ServiceProvider,因为ServiceProvider是一个abstract class,且定义了register()这个abstract function,所以继承的MyLaravelDebugbarServiceProvider必须实作register()

Illuminate/Support/ServiceProvider.php

1
2
3
4
5
6
7
8
9
10
11
12
namespace Illuminate\Support;

use BadMethodCallException;

abstract class ServiceProvider
{
...

abstract public function register();

...
}

register()有两个功能 :

  1. 让你手动register一个 service provider。
  2. 让你手动将一个 interface bind到指定 class。

第一个功能用在自己载入package,第二个功能用在管理自己的service container,在下个范例会看到。

在 register()注册

Illuminate/Support/ServiceProvider.php

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Register the application services.
*
* @return void
*/
public function register()
{
if ($this->app->environment() == 'local')
{
$this->app->register('Barryvdh\Debugbar\ServiceProvider');
}
}

由于 Laravel Debugbar 不适合在正式上线主机使用,因此我们特别判断application enviromnent是否为local,若为 local,才使用$this->app->register()注册Barryvdh\Debugbar\ServiceProvider,这相当于在config/app.phpproviders加入Barryvdh\Debugbar\ServiceProvider::class

注册自己的 Service Provider

config/app.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
'providers' => [

/*
* Laravel Framework Service Providers...
*/
Illuminate\Foundation\Providers\ArtisanServiceProvider::class,
(略)

/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\MyLaravelDebugbarServiceProvider::class,

],

config/app.php的最下方加入App\Providers\MyLaravelDebugbarServiceProvider::class,载入刚刚我们自己建立的MyLaravelDebugbarServiceProvider

也就是说,原本config/app.php是直接载入 Laravel Debugbar 提供的 service provider,现在改成载入自己写的 service provider,加入了判断 application environment,再自行载入 Laravel Debugbar 提供的 service provider,以避免在正式上线主机载入 Laravel Debugbar。

管理自己的 Service Container

如何对 Repository 做测试?中,我们曾经使用了Repository Pattern搭配 controller,不过当初并没有垫 interface,现在我们加上了PostControllerInterface,并使用 service provider 管理。

建立 Interface

app/Contracts/PostRepositoryInterface.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace App\Contracts;

use Illuminate\Database\Eloquent\Collection;

/**
* Interface PostRepositoryInterface
* @package App\Contracts
*/
interface PostRepositoryInterface
{
/**
* 传回最新3笔文章
*
* @return Collection
*/
public function getLatest3Posts();
}

定义PostRepositoryInterface,只有一个getLatest3Post()

实现 Interface

app/Repositories/PostRepository.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
namespace App\Repositories;

use App\Contracts\PostRepositoryInterface;
use App\Post;
use Illuminate\Database\Eloquent\Collection;

/**
* Class PostRepository
* @package App\Repositories
*/
class PostRepository implements PostRepositoryInterface
{
/**
* @var Post
*/
protected $Post;

/**
* PostRepository constructor.
* @param Post $Post
*/
public function __construct(Post $Post)
{
$this->Post = $Post;
}

/**
* 传回最新3笔文章
*
* @return Collection
*/
public function getLatest3Posts()
{
return $this->Post
->query()
->orderBy('id', 'desc')
->limit(3)
->get();
}
}

第 7 行

1
2
3
4
5
/**
* Class PostRepository
* @package App\Repositories
*/
class PostRepository implements PostRepositoryInterface

PostRepository class 实践了PostRepositoryInterface

app/Repositories/MyRepository.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
namespace App\Repositories;

use App\Contracts\PostRepositoryInterface;
use Illuminate\Database\Eloquent\Collection;

/**
* Class MyRepository
* @package App\Repositories
*/
class MyRepository implements PostRepositoryInterface
{

/**
* 传回最新3笔文章
*
* @return Collection
*/
public function getLatest3Posts()
{
$posts = new Collection();
for ($i = 1; $i 3; $i++) {
$post = [
'id' => $i,
'title' => 'My title' . $i,
'sub_title' => 'My sub_title' . $i,
'content' => 'My content' . $i,
];

$posts->push((object)$post);
}

return $posts;
}
}

第 6 行

1
2
3
4
5
/**
* Class MyRepository
* @package App\Repositories
*/
class MyRepository implements PostRepositoryInterface

MyRepository class 一样实践了PostRepositoryInterface

13 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 传回最新3笔文章
*
* @return Collection
*/
public function getLatest3Posts()
{
$posts = new Collection();
for ($i = 1; $i 3; $i++) {
$post = [
'id' => $i,
'title' => 'My title' . $i,
'sub_title' => 'My sub_title' . $i,
'content' => 'My content' . $i,
];

$posts->push((object)$post);
}

return $posts;
}

没到透过Post model 向资料库读取资料,而是自己用Collection凑 3 笔资料。

比较特别的是$post为阵列,所以要push进 collection 时,需要转型成object,否则 blade 在显示时会出错。

注入 Container

app/Http/Controllers/PostsController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
namespace App\Http\Controllers;

use App\Contracts\PostRepositoryInterface;
use App\Http\Requests;

class PostsController extends Controller
{
/**
* @var PostRepositoryInterface
*/
protected $posts;

/**
* PostsController constructor.
* @param $posts
*/
public function __construct(PostRepositoryInterface $posts)
{
$this->posts = $posts;
}

/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$posts = $this->posts->getLatest3Posts();
$data = compact('posts');
return View('posts.index', $data);
}
}

第 8 行

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @var PostRepositoryInterface
*/
protected $posts;

/**
* PostsController constructor.
* @param $posts
*/
public function __construct(PostRepositoryInterface $posts)
{
$this->posts = $posts;
}

将 repository 由 constructor 注入到 controller,注意现在$post的型别为PostRepositoryInterface,而不是PostRepository

切换 class

Service container 神奇的地方就在于任何有type hint的地方,Laravel 都会自动帮你载入物件,但若type hintinterface,由于实践该 interface 可能有很多物件,你必须使用App::bind()告诉 Laravel 该 interface 必须载入什麽物件,否则无法载入。

至于App::bind()该写在哪裡呢?Taylor 建议你写在service providerregister()

app/Providers/RepositoryServiceProvider.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Contracts\PostRepositoryInterface;
use App\Repositories\PostRepository;
use App\Repositories\MyRepository;

/**
* Class RepositoryServiceProvider
* @package App\Providers
*/
class RepositoryServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
//
}

/**
* Register the application services.
*
* @return void
*/
public function register()
{
$this->app->bind(
PostRepositoryInterface::class,
PostRepository::class
);
}
}

24 行

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Register the application services.
*
* @return void
*/
public function register()
{
$this->app->bind(
PostRepositoryInterface::class,
PostRepository::class
);
}

当你要注入的是PostRepository时,就 bindPostRepository::class,若要注入的是MyRepository时,就 bindMyRepository::class,controller 完全不用修改。

Register()与 Boot()

当我们使用php artisan make:provider建立 service provider 时,预设会建立register()boot(),之前已经讨论过register()是来自于ServiceProvider的 abstract method,所以我们必须实践,但boot()呢?boot()并不是ServiceProvider的 abstract method,所以我们可以不实践,但为什麽php artisan make:provider也帮我们建立了boot()呢?

当所有 service provider 的register()执行完后,接着会执行各 serive provider 的boot(),在

Laravel source 的ApplicationbootProvider()会去呼叫boot()

Illuminate/Foundation/Application.php

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Boot the given service provider.
*
* @param \Illuminate\Support\ServiceProvider $provider
* @return void
*/
protected function bootProvider(ServiceProvider $provider)
{
if (method_exists($provider, 'boot')) {
return $this->call([$provider, 'boot']);
}
}

所以 Laravel 并没有强迫要实践boot(),Laravel 再执行完所有 service provider 的register()之后,若你有实作boot()的话,就会来执行该 service provider 的boot()

到底什麽程式该写在 register()?什麽程式该写在  boot()呢?

register()应该只拿来写App::bind()App:register(),若要使用初始化物件,或使用其他相依物件,则应该写在boot(),有两个原因 :

  1. 根据SOLID单一职责原则register()只负责 service container 的 register 与 binding,boot()负责初始化物件。
  2. 若在register()使用其他相依物件,可能该物件还没bind,而导致执行错误;boot()在所有register()之后才执行,因此可以确保所有物件都已经bind

Deferred Providers

config/app.phpproviders中 service provider,都会在 Laravel 一启动时做 register 与 binding,若一些 service container 较少被使用,你想在该 service container 实际被使用才做 register 与 binding,以加快 Laravel 启动,可以使用deferred provider

加入$defer

app/Providers/RepositoryServiceProvider.php

1
2
3
4
5
6
7
8
9
10
11
12
class RepositoryServiceProvider extends ServiceProvider
{

/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = true;

...
}

在自己的 service provider 内加入$defer property 为 true。

加入 provides()

app/Providers/RepositoryServiceProvider.php

1
2
3
4
5
6
7
8
9
10
11
12
class RepositoryServiceProvider extends ServiceProvider
{
/**
* Get the services provided by the provider
*
* @return array
*/
public function provides()
{
return [PostRepositoryInterface::class];
}
}

provides()回传该 service provider 所要处理的完整 interface 名称。

删除 service.json

1
$ php artisan clear-compiled

所有要启动的 service provider 都会被 compile 在bootstrap/cache/service.json,因为我们刚刚将PostRepositoryServiceProvider改成deferred provider,所以必须删除service.json重新建立。

重新启动 Laravel

bootstrap/cache/service.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"providers": [
...
"App\\Providers\\RepositoryServiceProvider"
],
"eager": [
...
],
"deferred": {
...
"App\\Contracts\\PostRepositoryInterface": "App\\Providers\\RepositoryServiceProvider"
},
"when": {
...
}
}

Laravel 重新启动后,会重新建立service.json,在providers属性,会列出所有 service provider,因为我们刚刚将PostRepositoryServiceProvider加上$deffered = true,所以现在defferred属性会有该 service provider,而provides()所传回的 interface,正是物件的 property。

Conclusion

  • Service provider 提供了统一了大家写App::bind()之处。
  • register()内只应该写 register 与 binding,而boot()内只应该写初始化物件或使用其他相依物件。
  • Service provider 不单只是 package 会使用,也可以拿来管理 service container,将变化封装在 service provider 内,当将来需求变化时,只要修改 service provider 即可。

Sample Code

完整的范例可以在我的GitHub上找到。

  1. My Laravel Debugbar
  2. Repository with Interface
  3. Repository with Deferred

[转+] Laravel 中间件的介绍以及使用

介绍

Laravel 中间件提供了一种方便的机制来过滤进入应用的 HTTP 请求。
例如,Laravel 内置了一个中间件来验证用户的身份认证。如果用户没有通过身份认证,中间件会将用户重定向到登录界面。但是,如果用户被认证,中间件将允许该请求进一步进入该应用。
Laravel 自带了一些中间件,包括身份验证、CSRF 保护等。所有这些中间件都位于** app/Http/Middleware**``** **目录。

中间件的执行顺序说明

定义中间件

通过运行 make:middleware Artisan 命令来创建新的中间件:

1
php artisan make:middleware CheckAge

该命令将会在 app/Http/Middleware 目录下创建一个新的 CheckAge类,在这个中间件中,我们仅允许 age 参数大于 200 的请求对此路由进行访问,否则,我们将此用户重定向到 home

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace App\Http\Middleware;

use Closure;

class CheckAge
{
/**
* 处理传入的请求
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/

正如你所见,假如给定的 age 参数小于或等于 200 ,这个中间件将返回一个 HTTP 重定向到客户端;否则,请求将进一步传递到应用中。要让请求继续传递到应用程序中(即允许「通过」中间件验证的),只需使用 $request 作为参数去调用回调函数 $next

最好将中间件想象为一系列 HTTP 请求必须经过才能进入你应用的「层」。每一层都会检查请求(是否符合某些条件),(如果不符合)甚至可以(在请求访问你的应用之前)完全拒绝掉。

前置 & 后置中间件

一个中间件在请求之前或者之后进行任务处理,取决于中间件的代码逻辑放置的位置。

前置中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace App\Http\Middleware;

use Closure;

class BeforeMiddleware
{
public function handle($request, Closure $next)
{
// Perform action

return $next($request);
}
}

后置中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace App\Http\Middleware;

use Closure;

class AfterMiddleware
{
public function handle($request, Closure $next)
{
$response = $next($request);

// 执行操作

return $response;
}
}

注册中间件

全局中间件

假设你想让中间件在应用处理每个 HTTP 请求期间运行,只需要在 app/Http/Kernel.php 中的 $middleware属性中列出这个中间件

为路由分配中间件

假设你想为指定的路由分配中间件,首先应该在 app/Http/Kernel.php 文件内为该中间件分配一个 。默认情况下, Kernel 类的 $routeMiddleware 属性下包含了 Laravel 内置的中间件。若要加入自定义的中间件,只需把它附加到列表后并为其分配一个自定义 即可。例如:

1
2
3
4
5
6
7
8
9
10
// 在 App\Http\Kernel 类中

protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];

一旦在 Kernel 类中定义好了中间件,就可以通过 middleware 方法将为路由分配中间件:

1
2
3
Route::get('admin/profile', function () {
//
})->middleware('auth');

你也可以为路由分配多个中间件:

1
2
3
Route::get('/', function () {
//
})->middleware('first', 'second');

分配中间件时,你还可以传递完整的类名:

1
2
3
4
5
use App\Http\Middleware\CheckAge;

Route::get('admin/profile', function () {
//
})->middleware(CheckAge::class);

中间件群组

某些时候你可能希望使用一个 key 把多个中间件打包成一个组,方便将他们应用到路由中。你可以使用 Http kernel 的 $middlewareGroups 属性。

Laravel 内置了 webapi 两个中间件组,它们包含了常用的中间件,你可能会想应用到 web UI 和 API 路由中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 应用程序的路由中间件组
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],

'api' => [
'throttle:60,1',
'auth:api',
],
];

中间件组和单个中间件一样可以被应用到路由和控制器行为中。同时,中间组很方便得将多个中间件一次性应用到路由上:

1
2
3
4
5
6
7
Route::get('/', function () {
//
})->middleware('web');

Route::group(['middleware' => ['web']], function () {
//
});

中间件参数

中间件也可以接受额外的参数。举个例子,假如你的应用需要在执行特定操作之前验证用户是否为给定的 「角色」,你可以通过创建一个 CheckRole 中间件,由它来接收「角色」名称作为附加参数。他应该是放在$next 之后

附加的中间件参数应该在 $next 参数之后被传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

namespace App\Http\Middleware;

use Closure;

class CheckRole
{
/**
* 处理传入的参数
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string $role
* @return mixed
*/
public function handle($request, Closure $next, $role)
{
if (! $request->user()->hasRole($role)) {
// 重定向
}

return $next($request);
}

}

定义路由时通过一个 : 来隔开中间件名称和参数来指定中间件参数。多个参数就使用逗号分隔:

1
2
3
Route::put('post/{id}', function ($id) {
//
})->middleware('role:editor');

Terminable 中间件

有时中间件可能需要在 HTTP 响应发送到浏览器之后处理一些工作。比如,Laravel 内置的「session」中间件会在响应发送到浏览器之后将会话数据写入存储器中。如果你在中间件中定义一个 terminate 方法,则会在响应发送到浏览器后自动调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace Illuminate\Session\Middleware;

use Closure;

class StartSession
{
public function handle($request, Closure $next)
{
return $next($request);
}

public function terminate($request, $response)
{
// Store the session data...
}
}

terminate 方法应该同时接收和响应。一旦定义了这个中间件,你应该将它添加到路由列表或 app/Http/Kernel.php 文件的全局中间件中。

在你的中间件上调用 terminate 调用时,Laravel 会从 服务容器 中解析出一个新的中间件实例。如果要在调用 handleterminate 方法时使用同一个中间件实例,就使用容器的 singleton 方法向容器注册中间件。

参考文章

[转] laravel 的 模型 和 migration

参考文档:
http://www.golaravel.com/laravel/docs/5.0/migrations/
http://blog.hsin.tw/2015/laravel-5-note05-migrations/

通过命令行创建

1. 使用模型建立, 适用于没有数据表

这里适用的是没有数据表, 同时也没有模型, 这样创建模型时候便创建了一个 需要迁移的文件, 可以使用 artisan 命令来执行 迁移

1
2
3
$ php artisan make:model models\Article
Model created successfully.
Created Migration: 2015_03_25_232010_create_articles_table

这里使用的表创建的内容是复数形式的

2. 使用手动建立

以上建立的数据表是复数形式的, 所以可能不需要复数形式的数据表

可以使用 --no-migration 来禁止生成迁移文件, 然后再自动生成一个针对于指定数据表的迁移文件.

1
2
$ php artisan make:model models\Page --no-migration
Model created successfully.

以下命令生成了一个迁移文件, create 来创建

1
2
$ php artisan make:migration create_page_table --create=page
Created Migration: 2015_03_25_232934_create_page_table

如果是新建立一个更新的文件我们使用

1
2
$ php artisan make:migration add_ip_to_page_table --table=page
Created Migration: 2015_03_25_233227_add_ip_to_page_table

[转]将 MySQL Workbench 中已存在的数据表导出到 Laravel 迁移文件

原文地址: 将 MySQL Workbench 中已存在的数据表导出到 Laravel 迁移文件

今天我们接着介绍如何从 MySQL Workbench 中将已存在的数据表导出。

MySQL Workbench 是 MySQL 官方提供的跨平台 MySQL 客户端图形化操作软件,Brandon Eckenrode 为我们创建了一个插件,通过该插件我们可以将 MySQL Workbench 的模型导出为遵循 PSR-2 编码规范的 Laravel 迁移文件。在导出过程中,每个迁移文件会被生成并保存为名称与之相应的迁移文件。

要实现这一功能,我们需要从 Github(https://github.com/beckenrode/mysql-workbench-export-laravel-5-migrations)上下载这个插件,然后打开 MySQL Workbench,进入 Scripting 菜单,点击Install Plugin/Module,然后选择从 Github 项目下载下来的export-laravel-5-migrations.py文件,弹出安装成功的提示之后重启 MySQL Workbench。

选择File->New Model创建一个新表table_test,为该表添加一些基本的表结构,然后进入Tools菜单,选择Catalog->Export Laravel 5 Migration导出数据表到 Laravel 迁移文件:

这样会弹出一个确认窗口:

我们可以选择将其保存到指定目录。这样我们就可以一边在 MySQL Workbench 中创建数据表,一边生成 Laravel 迁移文件,两不耽误。

不过两者相比还是 Sequel Pro 导出功能更方便,更强大一些。

[转] 将 Sequel Pro 中已存在的数据表导出为 Laravel 迁移文件

原文地址: 将 Sequel Pro 中已存在的数据表导出为 Laravel 迁移文件

Laravel 开发者来说,主要有两种方式创建数据表,一种是通过 MySQL 客户端工具,比如 Sequel Pro(Mac 环境下比较流行的 MySQL 客户端软件),另一种是通过 Laravel 提供的 Artisan 命令生成迁移文件。

如果之前的项目不是通过 Laravel 构建的,现在要迁移到 Laravel,或者之前不是通过迁移文件生成的数据表,想要将已存在的数据表转化为 Laravel 迁移文件怎么办,由 Colin Viebrock 为 Sequel Pro 开发的Laravel 迁移导出工具为我们提供了方便。

我们从 github(https://github.com/cviebrock/sequel-pro-laravel-export)将代码克隆本地:

1
git clone https://github.com/cviebrock/sequel-pro-laravel-export.git

然后进入项目目录,双击ExportToLaravelMigration.spBundle文件,接下来在打开的页面连接到数据库,在左侧选中一张数据表,在菜单栏选择Bundles › Export › Export将数据表导出为迁移文件(或者使用快捷命令⌃⌥⌘M):

这样就会将选中数据表转化为 Laravel 数据库迁移文件并存放在桌面,比如我选中的是users表,对应的迁移文件是2016_11_20_212052_create_users_table.php,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

/**
* Migration auto-generated by Sequel Pro Laravel Export
* @see https://github.com/cviebrock/sequel-pro-laravel-export
*/
class CreateUsersTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name', 255);
$table->string('email', 255);
$table->string('password', 255);
$table->rememberToken();
$table->nullableTimestamps();

$table->unique('email', 'users_email_unique');

});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
}

我们可以将其拷贝到 Laravel 项目的数据库迁移目录,以便后续使用该文件进行后续操作。

[译 WIP][Plugin]判断导航元素的 active 状态 (hieu-le/active)

Laravel Active 用全新的API重写,并以新的主版本发布。这个文档解释了新API,并给出了一些例子。

安装

这个版本只与 Laravel 5 兼容。通过 Composer 来安装这个包

1
composer require hieu-le/active

添加服务提供者到 config/app.php  的  providers 数组

译者 : Laravel 5.5+ 之后的包可以自动发现, 不必添加这个 ServiceProvider

1
HieuLe\Active\ActiveServiceProvider::class,

如果您想要使用 alias,请在 config/app.php 中的 aliases 数组中注册。

译者 : Laravel 5.5+会自动注册

1
'Active' => HieuLe\Active\Facades\Active::class,

基于条件来获取活动类名

Usage:

-
Use the alias: Active::getClassIf($condition, $activeClass = 'active', $inactiveClass = '')

-
Use the application container: app('active')->getClassIf($condition, $activeClass = 'active', $inactiveClass = '')

-
Use the helper function: active_class($condition, $activeClass = 'active', $inactiveClass = '')

Explanation: if the $condition is true, the value of $activeClass is returned, otherwise the value of $inactiveClass is returned. The package comes with several methods to help you create conditions easier. You will get the class as a string as the result of these API.

1
2
3
4
active_class(true); // 'active'
active_class(false); // ''
active_class(if_uri([$currentUri]), 'selected'); // 'selected'
active_class(if_uri_pattern([$pattern1, $pattern2]), 'active', 'other'); // 'other'

检测当前 URI 地址

All of checking methods return boolean result (true or false). You can use the result in the condition of active_class or write your own expression.

Check the whole URI

Usage:

-
Use the alias: Active::checkUri(array $uris)

-
Use the application container: app('active')->checkUri(array $uris)

-
Use the helper function: if_uri(array $uris)

Explanation: you give an array of URI, the package will return true if the current URI is in your array. Remember that an URI does not begin with the slash (/) except the root.

Check the URI with some patterns

Usage:

-
Use the alias: Active::checkUriPattern(array $patterns)

-
Use the application container: app('active')->checkUriPattern(array $patterns)

-
Use the helper function: if_uri_pattern(array $patterns)

Explanation: you give an array of patterns, the package will return true if the current URI matches one of the given pattern. Asterisks may be used in the patterns to indicate wildcards.

Check the query string

Usage:

-
Use the alias: Active::checkQuery($key, $value)

-
Use the application container: app('active')->checkQuery($key, $value)

-
Use the helper function: if_query($key, $value)

Explanation: the package will return true if one of the following condition is true:

-
The current query string contains a parameter named $key with any value and the value of $value is false.

-
The current query string does not contain a parameter named $key and the value of $value is null.

-
The current query string contains a parameter named $key whose value is a string equals to $value.

-
The current query string contains a parameter named $key whose value is an array that contain the $value.

1
2
3
4
5
6
7
8
// the current query string is ?x=1&y[0]=a&y[1]=b

if_query('x', null); // true
if_query('x', 1); // true
if_query('x', 2); // false
if_query('y', 'a'); // true
if_query('y', 'c'); // false
if_query('z', null); // false

检测当前路由

Check the exact route name

Usage:

-
Use the alias: Active::checkRoute(array $routes)

-
Use the application container: app('active')->checkRoute(array $routes)

-
Use the helper function: if_route(array $routes)

Explanation: you give an array of route names, the package will return true if the name of the current route (which can be null) is in your array.

Check the route name with some patterns

Usage:

-
Use the alias: Active::checkRoutePattern(array $patterns)

-
Use the application container: app('active')->checkRoutePattern(array $patterns)

-
Use the helper function: if_route_pattern(array $patterns)

Explanation: you give an array of patterns, the package will return true if the name of the current route (which can be null) matches one of the given pattern. Asterisks may be used in the patterns to indicate wildcards.

Check the route parameter value

Usage:

-
Use the alias: Active::checkRouteParam($key, $value)

-
Use the application container: app('active')->checkRouteParam($key, $value)

-
Use the helper function: if_route_param($key, $value)

Explanation: the package will return true if one of the following condition is true:

-
The current route contains a parameter named $key whose value is $value.

-
The current route does not contain a parameter named $key and the value of $value is null.

Read more about route parameter in the Laravel documentation.

获取当前值

Get the current action

Usage:

-
Use the alias: Active::getAction()

-
Use the application container: app('active')->getAction()

-
Use the helper function: current_action()

Explanation: if the current route is bound to a class method, the result will be a string like App\Http\Controllers\YourController@yourMethod. If the route is bound to a closure, the result will be the Closure string.

Get the current controller class

Usage:

-
Use the alias: Active::getController()

-
Use the application container: app('active')->getController()

-
Use the helper function: current_controller()

Explanation: if the current route is bound to a class method, the result will be the full qualified class name of the controller class, like App\Http\Controllers\YourController. If the route is bound to a closure, the result will be the Closure string.

Get the current controller method

Usage:

-
Use the alias: Active::getMethod()

-
Use the application container: app('active')->getMethod()

-
Use the helper function: current_method()

Explanation: if the current route is bound to a class method, the result will be the name of the controller method. like yourMethod. If the route is bound to a closure, the result will be the empty string.

Example

The example below illustrate the usage of this package in a sidebar with Bootstrap link group:

1
2
3
4
5
6
7
8
9
10
11
<div class="list-group">
<a href="" class="list-group-item {{ active_class(if_route('users.list') && if_query('active', 1)) }}">
Active users
</a>
<a href="#" class="list-group-item {{ active_class(if_route('users.list') && if_query('active', 0)) }}">
Inactive users
</a>
<a href="#" class="list-group-item {{ active_class(if_action('App\Http\Controllers\UserController@getNewUser')) }}">
Add users
</a>
</div>

Laravel 6.0 升级到 6.x 记录

composer 2.0

composer 2.0 版本和 laravel 6.0 版本不兼容

这个是 laravel 6.0 lts 版本的问题, 由于更改了加载方式, 这个方式在 6.0 版本中没有被修复导致的问题, 可以查看 : https://github.com/composer/composer/issues/9340

对于 laravel 版本的支持程度可以查看

这里的解决问题的办法是强制使用 composer 1.x 版本

1
$ composer self-update --1

相关组件

为了使用 composer 2.x, 我把 laravel 6.0 升级到 6.x , 因为 laravel 自 6.18 之后才支持 composer 2, 同时升级的组件还有

1
2
3
4
5
6
7
8
"require": {
tucker-eric/eloquentfilter : "~2" => "3"
},
"require-dev": {
"itsgoingd/clockwork": "~4.0" => "~5.0"
"barryvdh/laravel-ide-helper": "~2.7" => "~2.*"
"doctrine/dbal": "^2.5" => "^3"
},

Method Monolog\Logger::addDebug() does not exist

重新命名一下之前的 ide-helper.php 重新发布一下配置

1
$ php artisan vendor:publish --provider="Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider" --tag=config

移除 Log 部分

1
2
3
4
5
6
7
8
9
10
11
12
	'magic' => [
- 'Log' => [
- 'debug' => 'Monolog\Logger::addDebug',
- 'info' => 'Monolog\Logger::addInfo',
- 'notice' => 'Monolog\Logger::addNotice',
- 'warning' => 'Monolog\Logger::addWarning',
- 'error' => 'Monolog\Logger::addError',
- 'critical' => 'Monolog\Logger::addCritical',
- 'alert' => 'Monolog\Logger::addAlert',
- 'emergency' => 'Monolog\Logger::addEmergency',
- ],
],

Laravel (Code Review) - 01

1. 带标签的缓存是无法不带标签删除的

1
2
3
4
5
6
7
8
9
#laravel 5.5
// 标签内部的数据外部是获取不到的
\Cache::tags('test')->remember('abc', SysConfig::HALF_DAY_MIN, function () {
return 5;
});
$a = \Cache::get('abc');
\Cache::forget('abc');
$b = \Cache::get('abc');
$this->assertEquals($a, $b);

2. 创建的数据和保存的数据不符合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 创建前
$input = [
"mobile" => "17081297193"
"password" => ""
"type" => "user"
"is_enable" => 1
];
$user = PamAccount::create($input);
$user = [
"mobile" => "18056346460"
"password" => ""
"type" => "user"
"is_enable" => 1
"updated_at" => "2018-07-26 22:07:30"
"created_at" => "2018-07-26 22:07:30"
"id" => 96
];

# 从数据库查询
$user = \User::find($user->id);
$user = [
"id" => 96
"mobile" => "18056346460"
"parent_id" => 0
"password" => ""
"type" => "user"
"is_enable" => 1
"login_times" => 0
"reg_platform" => "ios"
"created_at" => "2018-07-26 22:07:30"
"updated_at" => "2018-07-26 22:07:30"
]

I believe this is designed this way to limit number of SQL queries. If you need to get actual data saved in the database, you need to obtain this record explicitely, just the way you did :)

INSERT query doesn’t return actual row.

3. 事件中绝对不要返回 fasle

因为在 PHP 5.5 中 这样说明

Stopping The Propagation Of An Event
Sometimes, you may wish to stop the propagation of an event to other listeners. You may do so by returning false from your listener’s handle method.


4. 控制器方法

1
2
3
4
5
index()                  # 列表
establish($id = null) # 创建 / 编辑
update() # 更新 / 批量更新
delete() # 删除, 存在的订单不是彻底删除使用 delete
destroy() # 销毁(彻底删除)

5. 路由写法

路由器第二个参数不可以传 key

1
2
3
4
5
# 传值 bad
route('dsk_base_area.establish', ['parent_id' => $item['areaid']])

# 不传值 good
route('dsk_base_area.establish', [$item->areaid]])

这两个哪个写起来更简洁呢?
因为使用 route 的时候接收到的参数在控制器传参数进行获取
public function establish($parent_id) 这种方法才能够接收到数据的.
ps: 使用 $request->input('parent_id') 根本获取不到东西

6. 使用对象和对象的错误提示

1
2
3
4
5
6
7
# 使用对象的好处
route('dsk_base_area.create', [$item['areaid']]) # 如果不存在字段, 则报 undefined index 错误
route('dsk_base_area.create', [$item->area_id]) # 这里不报错的.

# 使用映射过的对象的好处是容易识记
route('dsk_base_area.create', [$item->areaid]) # 这里不报错的.
route('dsk_base_area.create', [$item->area_id]) # 使用映射过的字段更便于记忆, 减少浏览器的 `typo` 错误

7. 合理使用模型提供的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
# 取一条
UserMessage::where('item_id', $item_id)->select("*")->first();
UserMessage::where('item_id', $item_id)->first();
UserMessage::find($item_id);

# 取单个
UserMessage::where('item_id', $item_id)->lists('username')->first();
UserMessage::where('item_id', $item_id)->value('username');

# 模型方法
$item = UserMessage::find($item_id);
$item->num += 1;
$item->save();

8. Form 使用 post 方法提交可以不填写 ‘method’

1
2
3
4
5
6
# 这里来自于表单提交
@if (isset($item))
{!! Form::model($item,['route' => ['dsk_adv_item.edit', $item->id], 'id' => 'form_ad_place','method' => 'post']) !!}
@else
{!! Form::open(['route' => 'dsk_adv_item.create','id' => 'form_ad_place','enctype'=>'multipart/form-data']) !!}
@endif

优化后

1
2
3
4
5
@if (isset($item))
{!! Form::model($item,['route' => ['dsk_adv_item.edit', $item->id], 'id' => 'form_ad_place']) !!}
@else
{!! Form::open(['route' => 'dsk_adv_item.create','id' => 'form_ad_place']) !!}
@endif

9. 对于编辑/创建使用同一个模版

编辑和创建来说, 我们使用同一个模版, 模板的名字应该命名为 establish.blade.php

10. [批量]更新使用 update

因为这里的更新就是批量的, 并且使用的方式不是更新一个, 所以这里不使用 batchUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class AdPlaceController{
# bad
public function batchUpdate() {
// ...
}


# good
public function update() {
$update = \Input::input('update');
foreach ($update as $id => $item) {
AdvPlace::where('id', $id)->update($item);
}
return site_end('success', '更新成功');
}
}

11. 取消不需要的导入

1
2
3
4
5
6
use Order\Action\Hunter;
use Order\Models\Filters\OrderHunterFilter;
use Order\Models\OrderHunter;
use Order\Models\Resources\EarnResource;
use Site\Tests\Base\SiteTestCase;
use System\Models\PamAccount;

12. 类内部调用使用 self (self_accessor)

1
2
3
4
5
6
7
8
/**
* @param int $account_id 用户ID
* @return StatisticsRangeFilter
*/
public function account($account_id): self
{
return $this->where('account_id', $account_id);
}

13. Laravel 中 Carbon 对象可以直接进行时间传递

1
2
3
4
5
6
7
8
// deprecated
$rePublishTimer = Carbon::now()->subMinutes($interval)->toDateTimeString();
$accountIds = CustomHunter::where('updated_at', '<', $rePublishTimer)
distinct()->pluck('account_id');

// suggest
$accountIds = CustomHunter::where('updated_at', '<', Carbon::now()->subMinutes($interval))
distinct()->pluck('account_id');

14. 数据获取

获取数据应当采用最简单的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// bad : new 模型, 没有考虑逻辑
if ((new \System\Models\PamBind)->where('qq_key', $openId)->exists()) {
return $this->setError('已经绑定过');
}

PamBind::create([
'account_id' => $this->pam->id,
'qq_key' => $openId,
]);

// good : 优化后的代码
if (PamBind::where('qq_key', $openId)->exists()) {
return $this->setError('已经被绑定过');
}

PamBind::updateOrCreate([
'account_id' => $this->pam->id,
], [
'qq_key' => $openId,
]);

15.初始化异常处理

获取数据需要做异常处理, 否则会出现无查询结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// bad : 错误信息
public function init($id)
{
$this->appVersion = AppVersion::findOrFail($id);
$this->id = $this->appVersion->id;
return $this;
}

// good : 正确代码
public function init($id)
{
try {
$this->appVersion = AppVersion::findOrFail($id);
$this->id = $this->appVersion->id;
return true;
} catch (\Exception $e) {
return $this->setError($e->getMessage());
}
}

16. 队列中运行延迟时候需要确保存在 delay() 方法

使用 Illuminate\Bus\Queueable 这个 Trait,
.env 中的 QUEUE_DRIVER 能够是 sync

17. empty()Collection::empty() 方法不同

这段代码来自于清除未支付的中间订单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 原始代码
$list = FinancePayTransfer::where('status', FinancePayTransfer::STATUS_UNPAY)
->where('updated_at','<', Carbon::now()->subWeek()->subMinutes(30)->toDateTimeString())->select(['id'])->take(100)->get();
if ($list) {
foreach ($list as $k => $rs) {
FinancePayTransfer::where('id', $rs->id)->delete();
}
}


# 优化后代码
# [优化] Laravel 中可以直接传入对象进行操作返回, 所以这里不必再进行转换
$dayEnd = Carbon::now()->subWeek();
/** @var Collection $list */
$list = FinancePayTransfer::where('status', FinancePayTransfer::STATUS_UNPAY)
->where('updated_at','<', $dayEnd)
->take(100)->lists('id');

# [bug] 如果 $list 为空, 条目值为空, 因为$list 是对象, 所以 if ($list) 会一直是 true
# [优化] 减少数据库请求次数,使用 whereIn 方法替代数据库的循环删除方法.
if (!$list->isEmpty()) {
FinancePayTransfer::whereIn('id', $list)->delete();
}

18. 类名和文件名大小写匹配

如果不匹配会造成类在 unix 平台中无法匹配

19. 类方法名使用正确的方法名称

orderBy 是正确写法, 不是 OrderBy

1
2
3
4
5
6
7
8
9
# bad
AdvPlace::OrderBy('list_order', 'asc')->lists('title', 'id');

$DB->orderby('id', 'desc');

# good
AdvPlace::orderBy('list_order', 'asc')->lists('title', 'id');

$DB->orderBy('id', 'desc');

20. 生成 Laravel ide-helper 用来提示函数

使用以下命令可以生成代码提示

1
php artisan ide-helper:generate

21 使用 Map 会掉 el### 3.1 使用 Map 会坑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
$items   = [
[
'id' => 'a',
'type' => 'pc',
],
[
'id' => 'b',
'type' => 'web',
],
[
'id' => 'c',
'type' => 'pc',
],
];
$collect = collect($items)
->where('type', 'pc')
->map(function ($item) {
return $item;
});
/**
这里使用 where 之后是存在 Key 的
0 => array:2 [
"id" => "a"
"type" => "pc"
]
2 => array:2 [
"id" => "c"
"type" => "pc"
]
*/

// 解决方案
$data = collect();
$collect = collect($items)
->where('type', 'pc')
->each(function ($item) use ($data) {
$data->push($item);
});
/*
0 => array:2 [
"id" => "a"
"type" => "pc"
]
1 => array:2 [
"id" => "c"
"type" => "pc"
]
*/

22 使用 collect 转换成数组

toArray 递归转换成数组, 支持 toArray 方法的可以递归转换成数组

1
2
3
4
5
6
7
8
9
// collect 内部可以转换成数组
$collect = collect([1, 2, 3, 4]);
$colCollect = collect([$collect, $collect, $collect]);
$this->assertIsArray($colCollect->toArray()[0]);

// collect 不支持 toArray 模式的以数组展示
$std = new stdClass();
$colStd = collect([$std, $std, $std]);
$this->assertIsObject($colStd->toArray()[0]);