laravel服务容器(二)

#2 依赖注入 Dependency Injection

类对类存在依赖,我们希望的是解耦,即修改一个类的代码不需要去改动其他类的代码,在前面的工厂模式中已经很接近完成这个目标了,无论怎么修改类的代码我们都只需要修改工厂类的代码即可完成适配。

类对类存在依赖,实际上就是在这个类里面我们会需要一个一个类的实例来干一些事情,那么除了在自己的内部产生还有没有别的办法来获得一个对象呢?有的,正如#2的标题所说,注入!

什么叫注入呢?只要不是在类的内部new或者工厂make的实例,而是通过参数或者其他形式获取的实例其实都叫注入。

比如:

1
2
3
4
5
6
7
class House{
protected $memebrs

public function __construct($members){
$this->members = $members;
}
}

laravel服务容器(一)

laravel用了也挺久的了,曾经也想过深入研究读读源码什么的,但是尝试过几次都从心了。

正好寒假无聊得慌,然后就准备深入一点琢磨一下。

从服务容器开始(x 当初看laravel中文文档的时候,开头就是核心架构里面的服务容器什么的,把自己给怼自闭了

所以后来也没成功弄懂,后来倒是写过一次IoC的实例。但是到现在也忘光了(x

依赖注入和控制反转是对同一件事情的不同描述,它们描述的角度不同。

  • 依赖注入是从应用程序的角度在描述,应用程序依赖容器创建并注入它所需要的外部资源。
  • 控制反转是从容器的角度在描述,容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。

这个可能并不一定看的太懂,但是之所以要使用这一种模式关键还是为了解耦。


#0 常规操作

面向对象的程序开发离不开类、接口、对象这些东西。然后万物皆对象,房子可以是一个对象,人也是可以是一个对象,那么人在房子里。这两个对象就扯上关系了,他们之间就存在着一个依赖。

就用上面的例子,房子,人这个时候可能会这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Human {
protected $name;
protected $age;

public function __construct($name,$age)
{
$this->name = $name;
$this->age = $age;
}
}

class House {
protected $human;

public function __construct()
{
$this->human = new Human('咕咕咕', 18);
}
}

对于小型项目寥寥几个类这样的依赖并不会发生什么不愉悦的事情,就算业务发生变化有什么需要修改的事情也只需要修改寥寥几个地方。比如在上面的地方只有在House类的内部有一个new People

但是如果一个项目大了起来,类开始多了起来,然后依赖变得复杂起来。从一对一变成一对多甚至多对多的时候就是噩梦一般的体验了。比如:

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
class Human {
protected $name;
protected $age;

public function __construct($name,$age)
{
$this->name = $name;
$this->age = $age;
}
}

class Dog {
protected $name;
protected $age;

public function __construct($name,$age)
{
$this->name = $name;
$this->age = $age;
}
}

class Cat {
protected $name;
protected $age;

public function __construct($name,$age)
{
$this->name = $name;
$this->age = $age;
}
}

class House {
protected $human;
protected $dog;
protected $cat;

public function __construct()
{
$this->human = new Human('咕咕咕', 18);
$this->dog = new Dog('汪汪汪', 2);
$this->cat = new Cat('喵喵喵', 2);
}
}

#1 工厂模式 Factory Pattern

在上面的代码中,如果Cat类的构造函数,或者类名什么的发生了变化,那么就需要修改House的代码,可能发感觉不到什么,那么如果有其他的东西也对猫有依赖呢?比如有一个CatHouse类,有一个CatClimb类,或者更多?那么是不是意味着修改一个Cat一个构造函数就需要修改多个类?这将会是多么糟糕的体验?

所以为了避免这种事情,后来就有人提出了工厂模式,什么叫工厂模式呢?Show code。

1
2
3
4
5
6
7
8
9
10
11
class BiologyFactory
{
public function make($type, $options)
{
switch ($type) {
case 'human' : return new Human($options[0], $options[1]);
case 'dog' : return new Dog($options[0], $options[1]);
case 'cat' : return new Cat($options[0], $options[1]);
}
}
}

然后呢,房子类就可以这么写了:

1
2
3
4
5
6
7
8
9
10
11
class House {
protected members;

public function __construct($members)
{
$factory = new BiologyFactory;
foreach($members as $member => $options){
$this->members[] = $factory->make($member, $options);
}
}
}

试想,当我们这么操作之后,如果我们修改了Cat类,Dog类还需要修改House类的代码嘛,很明显是不需要的。又如同我们之前说的如果又多个类同时依赖Cat类,我们修改了Cat类的构造方法也再无必要去修改多个依赖了他的类,我们需要做的只有修改工厂类即可。

在这个过程中,我们相当于在一对多或者对多对的依赖中添加了一层工厂,原本类直接对类产生依赖,现在我们在类与类之间添加了一个工厂,让类依赖工厂,通过工厂来获取原来依赖的对象。这么一来是不是舒服很多了呢?

然而人们追求优雅的脚步是永无止尽的,既然可以做到像上面的类只需要依赖一个类,那么有没有可能做到完全的解耦呢?不管怎么修改我们都不需要去修改类的代码。

比如我们现在如果修改了Cat类,我们只需要修改工厂类的代码即可,那么有没有可能不需要修改工厂类的代码呢?再或者说,如果添加了更多的生物类我们就需要去修改工厂类的代码为switch添加更多的case。有没有可能不修改类的代码而是通过其他的方法来实现呢?

以及,这样的工厂真的就是最优雅的了吗?如果同一个生物有不同的构造方法,那么该怎么处理呢?是不是同一个类需要多个case来进行处理呢?这是不是意味着工厂生产东西的对象类型有些单一了呢?

所以,上述的问题都会被解决。

ES6的this

MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this

js的function关键词定义函数体内的this指向在定义的时候是不确定的,只有在定义的函数被调用的时候才会赋予里面的this一个指向,而这this指向的是直接调用者。

什么是直接调用者呢?比如我要调用a对象的b方法,那么这个时候b的直接调用者就是a,b内的this指向就是a。

又比如我要调用a对象的b属性的c方法,那么c的直接调用者就是b。a通过b间接调用了c方法。

算了,这么写着太乱了,直接举例子:

1
2
3
4
function x() {
return this;
}
x(); //window

直接使用函数的话则会指向全局对象,浏览器环境下是window。在node环境下则是global。

而如果在strict模式下则会指向undefined

1
2
3
4
5
6
let a = {
b : function() {
return this;
}
}
a.b(); //返回的是a对象
1
2
3
4
5
6
7
8
let a = {
b : {
c : function() {
return this;
}
}
}
a.b.c(); //返回的是b对象。
1
2
3
4
5
6
7
let a = {
b : function() {
return this;
}
}
let c = a.b;
c(); //返回的是全局对象 window/global

最后一个虽然调用的函数还是定义在a里面的b,但是直接调用者其实还是全局对象,let c = a.b中把定义的函数赋值给了c。

1
2
3
setTimeout(function(){
console.log(this);
},100); //window
1
2
3
4
5
6
function x(){
setTimeout(function(){
console.log(this);
},100);
}
x(); //window
1
2
3
4
5
6
7
8
let x = {
y : function() {
setTimeout(function(){
console.log(this);
},100);
}
}
x.y(); //window

这里可以看出在setTimeout内使用function定义的闭包实际上都会由window来直接调用。

然后还有一种情况就是new。

js中通过function定义的方法其实都是一个Function的实例。

比如:

1
2
3
4
5
6
function Fn(){
this.x = 'aaa';
}

let x = new Fn();
console.log(x); //Fn {x: "aaa"}

可以看到在Fn中this的指向变成了他自自己,所以在尝试输出x的过程中有一个属性叫x,这个x是在Fn()内的this.x = 'aaa'定义的。

实际上在let x = new Fn()的过程中,new操作符改变了Fn()中this的指向,将他指向了新创建的对象。

1
2
3
4
5
6
7
8
9
function Fn(){
this.x = 'aaa';
return {
y : 'bbb'
}
}

let x = new Fn();
console.log(x); //{y: "bbb"}
1
2
3
4
5
6
7
function Fn(){
this.x = 'aaa';
return 'bbb';
}

let x = new Fn();
console.log(x); //Fn {x: "aaa"}

至于这是为什么的话。。不太清楚(x,可能跟new有关,可能要另外写一篇。

唔。。看了一下官方的文档:

在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值:

  • 如果是该函数是一个构造函数,this指针指向一个新的对象
  • 在严格模式下的函数调用下,this指向undefined
  • 如果是该函数是一个对象的方法,则它的this指针指向这个对象
  • 等等

然后function部分写完了就是箭头函数了,算是一种特殊的匿名函数。语法:

1
2
3
(parm) => {
//do something
}

那么在匿名函数里面,this指向的是哪里呢?

箭头函数中,this与封闭词法环境的this保持一致。在全局代码中,它将被设置为全局对象

什么叫封闭词法环境呢?我不明白

我的理解就是箭头函数的this被设置为他被创建时的环境。他被创建的环境是y内的z。而z的this指向y,所以它的this也是y。

比如下面的代码。我的箭头函数写在setTimeout()内,然后他的上一层对象应该是x.y

1
2
3
4
5
6
7
8
9
10
let x = {
y : {
z : function() {
setTimeout(() => {
console.log(this);
},100);
}
}
}
x.y.z() //返回的是y对象。

说实话,还是有些不太明白,这篇文章还需要经过修改(x

玩一玩ScriptOJ(二)

继续摸ScriptOJ

#5

有一只狗,不允许别人摸它,一旦摸它就会叫,然后就跑了。

完成 Dog 组件,当用户点击的时候会执行自身的 barkrun 方法。

已经给了代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Dog extends Component {
bark () {
console.log('bark')
}

run () {
console.log('run')
}

render () {
return (<div>DOG</div>)
}
}

看起来跟事件有关?估计有一个click什么的事件吧。看来得翻一下文档。

唔。。看了一下,似乎可以这么操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Dog extends Component {
bark () {
console.log('bark')
}

run () {
console.log('run')
}

render () {
return (<div onClick={() => {this.bark(); this.run();}}>DOG</div>)
}
}

大概就是把一个闭包绑定到了onClick这个属性上面,至于onClick是不能是html标签本身的onclick属性我就不是很明白,但是这个代码AC了,所以先过。。。

#6

有一只狗,不允许别人摸它,一旦摸它就会叫,然后就跑了;这只狗跑一段时间(20~50ms)以后就会停下来,也不叫了。

完成 Dog 组件,当用户点击的时候会执行自身的 barkrun 方法。给这个 Dog 组件加上状态 isRunningisBarking,在进行相应的动作的时候设置为 true,停下来的时候设置为 false

已经给了的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Dog extends Component {
constructor () {
super()
/* TODO */
}

bark () {
/* TODO */
}

run () {
/* TODO */
}

render () {
return (<div>DOG</div>)
}
}

直接怼:

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
class Dog extends Component {
constructor () {
super();
this.state = {
isRunning : false,
isBarking : false
};
}

bark () {
this.setState({isBarking: true});
}

run () {
this.setState({isRunning: true});
setTimeout(() => {
this.setState({isBarking: false});
this.setState({isRunning: false});
}, Math.random(20,50))
}

render () {
return (<div onClick={() => {this.bark(); this.run()}}>DOG</div>)
}
}

组件的state可以理解成组件的私有属性,这个私有属性应该是属于某一个组件实例的,至于setState()应该是一个定义在Component类上的方法,我一开始想直接this.state.isRunning = true但是他不让我过。说起来这样写可能确实不符合规范吧。然后还有些问题就是我直接在run()里面setTimeout()总感觉有些不太雅观,ac是ac了,但是有些难受,所以我打算看一下别人是怎么做的。

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
class Dog extends Component {
constructor () {
super()
this.state = {
isRunning: false,
isBarking: false
}
}

bark () {
console.log('bark')
this.setState({ isBarking: true })
setTimeout(() => this.setState({ isBarking: false }), 20)
}

run () {
console.log('run')
this.setState({ isRunning:true })
setTimeout(() => this.setState({ isRunning: false }), 20)
}

handleClickOnDog () {
this.bark()
this.run()
}

render() {
return (
<div onClick={this.handleClickOnDog.bind(this)}>DOG</div>
)
}
}

写了一个新的方法叫handleClickOnDog,为啥呀,而且他停止跑和吠的时间居然不是固定的,看来这不是一个测试点?而且在onClick里面写的是this.handleClickOnDog.bind(this)看起来像是给这个组件绑定上了一个像事件监听器的东西,这个好像没了解过,不行,得看看。

官方的文档在讲到事件处理的时候给了这样的一份代码:

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
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};

// 为了在回调中使用 `this`,这个绑定是必不可少的
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}

render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}

ReactDOM.render(
<Toggle />,
document.getElementById('root')
);

其中的注释:// 为了在回调中使用 this,这个绑定是必不可少的

但是如果使用箭头函数的话则不需要绑定this,这个好像跟箭头函数与普通的function中this指向的对象有关。

不行这个我要另外写一篇(x


琢磨了半天this是个啥。。所以今天先摸了(x

今天大概了解了React的事件以及他的state吧。。

虽然也并不是特别的清楚。

玩一玩ScriptOJ(一)

咕咕咕,咕咕咕

刚有些无聊正打算这今天的每日一摸该写些什么,然后突然想起了前段时间学长在前端组的群里发了一个链接,http://scriptoj.mangojuice.top/点开看了一下,注册了一个账号就开始给我出题了。


见面礼

完成 extname 函数,它会接受一个文件名作为参数,你需要返回它的扩展名。

例如,输入 emoji.png,返回 .png

一开始我寻思着这个比较简单,应该可以写的简短一点

1
const extname = (filename) => `.${filename.split('.').pop()}`

然后…代码写的有点丑,并且:

image-20200201195642863

好吧,考虑不周

这里需要考虑的情况好像有几个

xxxxxx.xxx.xxx。遂:

1
2
3
4
5
6
7
8
9
10
11
12
13
const extname = (filename) => {
let filenameArr = filename.split('.');
if(filenameArr.length === 1){
//xxx类型
return '';
}else{
if(filename[0] === '.'){
return '';
}else{
return '.' + filenameArr.pop();
}
}
}

OK,Accepted。不过如果遇到.xxx.xxx这样的文件扩展名到底是什么呢(x


然后。。就给我跳出了好多道题目啊。。。怎么说呢,很爽!

但是题目好像也不是很多的样子啊。。。那就从头做到尾吧


#0

在 id 为 contentdiv 中只显示 Hello World 字样。(注意不要有多余空白字符)


1
<div id="content">Hello World</div>

唔。。是真的Hello World了(x

#1

在页面上增加一个 idroot元素。然后请你完成一个 `renderContent` 函数,这个函数会把传入的任意字符串都包装到一个 元素中并且渲染到页面上。例如:

1
renderContent('Hello World')

页面上就有相应的内容:

1
2
3
<div id='root'>
<h1>Hello World</h1>
</div>

只能通过 React.js 来实现。


完蛋,React.js我没学过呀 呜呜呜。那咋办呢,只好现学啦(x

大概了解了一下,在React下好像用了一种叫jsx的模板,可以写出这样子的代码:

1
const h1 = <h1>Hello World</h1>;

有一种html和js混在一起了的感觉,然后这种写法应该是等价于:

1
React.createElement("h1", null, "Hello World");

的,有些好奇,那么嵌套的标签会怎么解释呢。试试:

1
2
3
<div id="root">
<h1>Hello World</h1>
</div>

结果是:

1
2
3
React.createElement("div", {
id: "root"
}, React.createElement("h1", null, "Hello World"));

看起来第二个参数传递的是元素的属性,应该是通过对象以键值对的形式传递进去。

然后观察了一下,似乎可以通过这样的方式去实现

1
2
3
4
ReactDOM.render(
<h1>Hello World</h1>,
document.getElementById('root')
);

那么问题又来了,我应该怎么注入变量到里面呢,然后又回到了jsx的特性,通过大括号{},就是,要写成这样

1
2
3
4
ReactDOM.render(
<h1>{ content }</h1>,
document.getElementById('root')
);

所以猜测应该可以这么写:

1
2
3
<div id='root'>

</div>
1
2
3
4
5
6
function renderContent (content) {
ReactDOM.render(
<h1>{content}</h1>,
document.getElementById('root')
);
}

ok,Accepted.

#2

使用 React.js 构建一个未读消息组件 Notification

通过 getNotificationsCount() 来获取未读消息的数量 ,如果有未读消息 N 条,而且 N > 0,那么 Notification 组件渲染显示:
a

1
<span>有(N)条未读消息</span>

否则显示:

1
<span>没有未读消息</span>

(你只需要完成组件部分,不需要调用 ReactDOM

给定了需要的代码的一部分:

1
2
3
4
5
6
7
// 函数 getNotificationsCount 已经可以直接调用

class Notification extends Component {
render () {
// TODO
}
}

了解了一下,extends 的这个Component好像就是React的组件,然后他这个render()方法定义或者说声明了如何渲染这个组件。

既然如此,他如果希望我返回一个<span>有(N)条未读消息</span>或者<span>没有未读消息</span>的样子其实就很简单了,我只需要在返回之前无所不用其极获取到需要的数据,然后给他注入到组件里边,然后return出去、


故猜测应该这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Notification extends Component {
render () {
let count = getNotificationsCount();
if(count === 0){
return(
<span>没有未读消息</span>
);
}else{
return(
<span>有({count})条未读消息</span>
)
}
}
}

ok,Accepted.

#3

用 JSX 完成两个变量的定义:

第一个变量 title 为一个具有类名为 title<h1> 元素,其内容为 ScriptOJ

第二个变量 page 为一个具有类名为 content<div> 元素,将之前定义的 title 变量插入其中作为它的内容。


好像比较简单,按照#1的样子,jsx定义变量其实就是直接写上去就完事了,然后模板会帮我把他翻译成React.createElement,所以:

1
2
const title = <h1 class="title">ScriptOJ</h1>;
const page = <div class="content">{title}</div>;

然而提交之后说我wa了,原因是title 中的 'h1.title' 数量应该有 1 个,但你定义的 title 中有 0 个.

嗯?? 看了一眼讨论区,他这个类名,应该是className。

我好像有哪里没弄明白。。然后看了一会儿菜鸟教程,在里面看到了。。。

注意,在添加属性时, class 属性需要写成 className ,for 属性需要写成 htmlFor ,这是因为 class 和 for 是 JavaScript 的保留字。

好吧我知道了,那么答案就是

1
2
const title = <h1 className="title">ScriptOJ</h1>;
const page = <div className="content">{title}</div>;

ok,Accepted.

#4

一个房子里面有一个房间和一个洗手间,房间里面有一个人和两条狗。

请你完成组件:HouseRoomBathroomManDog,它们的最外层都用 div 标签包裹起来,类名分别为:houseroombathroommandog

组件的实现应该具有上述的嵌套关系。

已经给了的代码有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Component 已经可以直接使用

class House extends Component {
// TODO
}

class Room extends Component {
// TODO
}

class Bathroom extends Component {
// TODO
}

class Man extends Component {
// TODO
}

class Dog extends Component {
// TODO
}

看起来,也不是很复杂,就是很简单的嵌套,在别的组件里面也是可以直接调用定义的别的组件的。所以直接写:

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
// Component 已经可以直接使用

class House extends Component {
render(){
return (
<div className="house">
<Room />
<Bathroom />
</div>
)
}
}

class Room extends Component {
render(){
return (
<div className="room">
<Man />
<Dog />
<Dog />
</div>
)
}
}

class Bathroom extends Component {
render(){
return <div className="bathroom"></div>
}
}
class Man extends Component {
render(){
return <div className="man"></div>
}
}
class Dog extends Component {
render(){
return <div className="dog"></div>
}
}

ok. Accepted


React挺有意思的,感觉跟vue有点像,但是他们定义组件的方式感觉又有些不同,也不能说不同,唔。。感觉,都还行?感觉学起来比vue轻松那么一丢丢,可能是错觉吧 2333

今天先到这里,明天继续怼,如果有空并且记得的话 嗯。

Laravel的Eloquent

在对laravel的理解一文中,结尾提到了一下Model

然后考虑到篇幅限制,以及暂时用不到,所以就没有做更详细的阐述

说到Model,就必然离不开数据库,因为Model就是一个后端应用的数据层。他的位置应该是在控制器与数据库之间,负责的部分是直接与数据库进行交互,返回数据给控制器。

在知道model是干什么的情况下,我们需要做什么呢?

我们现在的数据库还没创建好,所以首先创建好数据库

注意,如果使用mysql的话,关于数据库的部分配置在.env文件内:

1
2
3
4
5
6
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=root

内,配置好用户名和密码之后,我们需要做的就是创建一个和上面DB_DATABASE配置项同名的数据库。

如果使用navicat或者phpAdmin可能会比较轻松,如果使用mysql命令的话大概是登陆进数据库之后create database laravel

然后剩下的部分,都通过php代码完成。

假设存在这样的一个表:

image-20200130190618309

这个表有6个字段,分别是

  • id作为每一条数据的唯一识别编号
  • user_id用来标识与这个post相关联的用户
  • title用于记录一篇文章的标题
  • content用于记录一篇文章的内容
  • created_atupdated_at用于记录这篇文章的创建时间和上一次更改的时间

当我在把一个东西(这里以一篇文章为例)存进数据库的时候,其实就是这样的操作,想一下需要储存这个东西的哪些数据,然后列出来,其实就设计了这么样的一个表。

现在我们知道一条文章的数据有哪些属性了,我们该怎么在数据库里建立这么一个表呢?答案是通过laravel的migrate机制,也就是迁移。

我通过在根目录执行一条命令:

1
php artisan make:migration create_posts_table

然后就能观察到我的项目根目录下的/database/migrations下多了一个文件,文件名应该是2020_0x_xx_xxxxxx_create_yyyyy_table.php,x和你创建这个迁移的时间有关,yyyy就是执行命令make:migration输入的参数,按照规范需要创建一个表的时候一般create_<表名>_table,其中表名按照规范一般是储存的东西的小写负数形式,比如posts

创建完的文件是这样的

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
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('posts');
}
}

这是一个继承了Migration的类,他的里面有两个方法,一个是up一个是down,其中up是在执行迁移的时候调用的,down是在回滚迁移的时候用的。

然后需要修改的部分就是

1
2
3
4
Schema::create('posts', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamps();
});

被包在function中间的部分,按照前面的表结构,我应该这么写:

1
2
3
4
5
6
7
Schema::create('posts', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id');
$table->string('title');
$table->string('content');
$table->timestamps();
});

至于为什么这么写自己比较与观察,然后查看官方文档的数据库迁移部分

设计好表之后执行

1
php artisan migrate

关于前面提到的回滚:比如当我创建了一个表,并且已经迁移进了数据库发现少写了一些属性并且代码没有上传的时候,就可以执行

1
php artisan migrate:rollback --step=1

回滚上一个迁移

然后能看到

1
2
Migrating: 2020_01_30_110448_create_posts_table
Migrated: 2020_01_30_110448_create_posts_table (0.02 seconds)

告诉我迁移成功了。

数据表创建完成了,然后我们就需要为这一个储存的东西定义一个Model

正如对laravel的理解一文的结尾所说,一个应用可能会有很多种数据呢,比如用户啦,比如文章啦。laravel不可能帮我们尽善尽美的都定义好,所以这个数据模型的模板就需要我们自己来定义。

1
php artisan make:model Post

然后就能看到在项目的根目录下的/app内多了一个Post.php,初始内容如下

1
2
3
4
5
6
7
8
9
10
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
//
}

这个东西就是我们会经常用到的laravelModel也叫Eloquent模型啦

然后关于Model的更多东西请一定要去官方文档看,因为这东西他用处很多很多,我只讲讲最基础的增删改查什么的,这个的文档在:https://learnku.com/docs/laravel/6.x/eloquent/5176

这里给大家介绍一个东西叫做tinker,这是一个交互式编程的环境,可以通过这条命令进入

1
php artisan tinker

跟数据库有关的有什么东西呢都?无非就是增删改查啦。

单条数据的增加我们可以这样

1
2
3
4
5
$post = new Post;
$post->user_id = 1;
$post->title = 'gugugu';
$post->content = 'ge zi lai le';
$post->save();

当然也可以这么写:

1
2
3
4
5
Post::create([
'title' => 'gugugu',
'content' => 'ge zi lai le',
'user_id' => 1
]);

但是这么写的话是一种批量赋值的手法,需要我们在model内定义什么样的属性允许批量赋值,比如:

1
2
3
4
class Post extends Model
{
protected $guarded = ['id', 'user_id'];
}

就是,除了iduser_id之外的东西都可以批量被赋值给新的post

同样也可以声明一个$fillable来声明哪些属性是允许被批量赋值的,简单地说的话

$guarded 是黑名单而 $fillable 是白名单

当需要根据id来查找一条数据,也就是获取一个Post实例:

1
$post = Post::find($id);

当需要根据某个条件来查找一条数据,比如title

1
2
3
//获取一条数据,也就是一个Post实例 ->first()
//获取第一个文章标题是gugugu的文章
$post = Post::where('title','gugugu')->first();
1
2
3
//获取所有数据,返回的会是一个集合,可以通过foreach进行遍历 ->get()
//获取所有user_id大于1的文章
$post = Post::where('user_id','>',1)->get();
1
2
3
4
5
//也支持多个条件的说
$post = Post::where([
'user_id' => 1,
'title' => 'gugugu'
])->first();

删除十分简单,在前面的查找获取到了某个实例之后,只需要调用

1
$post->delete();

即可删除数据

修改同样十分简单,直接修改实例的属性然后save即可,例:

1
2
$post->title = 'bugu bugu bugu';
$post->save();

ES6的Proxy(一)

前段时间看阮一峰老师的es6入门向大家提了一点关于解构赋值的问题之后,有个学长说es6最难懂的应该是Proxy了,所以说有难度的东西就应该挑战着玩一下的说。

然后看了一下觉得怪有意思的。所谓的Proxy,是一个用于修改某些操作的默认行为,原话是:

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

说实话,我并不是很能明白这段话说了什么。简单的学习来看,Proxy给了我一种人如其名的感觉,以网络为原型的话相当于在目标主机与请求方之间建立了一个代理,然后可以肆意的操作两边来往的数据。又有点像laravel的中间件,又有点像python的装饰器。

一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let person = {
name: '咕咕咕',
age: 38
}
let proxy = new Proxy(person, {
get : (target, key) => {
if(key == 'age') {
return target[key] > 18 ? 18 : target[key];
}else{
return target[key];
}
}
})
console.log(proxy.age) //18

这里新建了一个Proxy实例,定义了一个针对获取person对象的属性的拦截行为,即get

一般而言,调用person对象的属性会这么写成这样:person.age,其中的age就是属性名,然后person是对象,然后在Proxy也就是代理内部的话就可以获取到这个对象以及调用属性名,就是targetkey

然后再调用proxyget方法的时候实际上就执行了定义在Proxyget方法。

写在Proxy内的方法的参数名字并不重要,顺序即可似乎。

Proxy支持的拦截操作似乎较多,但是很多都是目前的我不甚熟悉的,所以只做摘录(x

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

对于get方法而言,允许传入的三个参数分别是targetkeyreceiver,分别代表着

  • 代理的目标
  • 获取的参数
  • 读操作所在对象(一般就是proxy的实例)

然后有些注意事项就是即使在proxy内也不能修改来自targetread-only and non-configurable,至于为什么要有这个规则不是很符合我的理解。这不符合一个作为中间件或者代理服务器的行为,说明我对他的理解出现了一些偏差。

比如这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let x = Object.defineProperties({},{
age : {
value : 19,
writable : false,
configurable: false
},
name : {
value : "咕咕咕",
writable : false,
configurable: false
},
})

let proxy = new Proxy(x,{
get: (t,k) => 18
})
console.log(proxy.age)

这样的代码当xage确实是18的时候,可以正常返回18

但是当x.age不是18的时候则会遇到报错:

1
TypeError: 'get' on proxy: property 'age' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '19' but got '18')

让人感到有些困惑。

饥荒联机版mod开发摸鱼(二)

今天又遇到了一个全新的问题,就是把昨天的鱼塘mod配方改了,然后可以做出来鱼塘了,但是这个鱼塘他并不会生产东西了。于是我今天继续琢磨起来了(x

在每个prefabs的看着很像构造方法的fn内一般能看到许多的inst:AddComponent操作,似乎是给这个实例绑定一个部件,嗯。。也有一种接口的意思,比如这段代码

1
2
3
inst:AddComponent("harvestable")
inst.components.harvestable:SetUp("pondfish", 3, nil, onharvest, updatelevel)
inst:ListenForEvent("childgoinghome", onchildgoinghome)

就是给当前实例绑定一个harvestable接口,让这个实例变成一个可被收获的实例,然后第二行进一步设置收获的规则,这里调用了一个harvestable实例的SetUp的函数,这个类的代码在scripts/components/harvestable.lua,代码如下

1
2
3
4
5
6
7
function Harvestable:SetUp(product, max, time, onharvest, ongrow)
self:SetProduct(product, max)
self:SetGrowTime(time)
self:SetOnGrowFn(ongrow)
self:SetOnHarvestFn(onharvest)
self:StartGrowing()
end

各个参数的语义也很清晰,然后看看这个接口是怎么实现的。在开头部分可以看到一部分类的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
local Harvestable = Class(function(self, inst)
self.inst = inst
self.produce = 0
self.growtime = nil
self.product = nil
self.ongrowfn = nil
self.maxproduce = 1
end,
nil,
{
produce = onproduce,
}
)

不得不说,lua这个闭包是真的很丑,虽然我也不清楚这个能不能算得上是一种闭包,但是从代码大概可以知道这个类会有哪些属性,好的我们继续。回到Harvestable:SetUp方法,可以看到方法体内部有5行。前面四行都是设置一些类属性的,然后最后调用了一个没有参数的方法,好的 那个很可疑,遂找到方法体:

1
2
3
4
5
6
7
8
function Harvestable:StartGrowing(time)
self:StopGrowing()
local growtime = time or self.growtime
if growtime then
self.task = self.inst:DoTaskInTime(growtime, function() self:Grow() end, "grow")
self.targettime = GetTime() + growtime
end
end

大意是,如果growtime有值的话就执行一个inst:DoTaskInTime,那么问题来了,这个代码好像不好找。

好了我又回来了,通过全局搜索DoTaskInTime(time找到了一个疑似该方法的定义,位于scripts/entityscript.lua文件内,代码如下:

1
2
3
4
5
6
7
8
9
10
11
function EntityScript:DoTaskInTime(time, fn, ...)
--print ("DO TASK IN TIME", time, self)
if not self.pendingtasks then
self.pendingtasks = {}
end

local per = scheduler:ExecuteInTime(time, fn, self.GUID, self, ...)
self.pendingtasks[per] = true
per.onfinish = task_finish -- function() if self and self.pendingtasks then self.pendingtasks[per] = nil end end
return per
end

对不起,看着有点复杂,涉及的可能会有点多,所以我决定先掠过一下,留下一个猜测根据scheduler:ExecuteInTime这是一个让某个函数在某个时间点后执行的方法。

然后在StartGrowing(time)内传入的是一个function() self:Grow() end的很像闭包的东西,好的然后找到中间那个方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Harvestable:Grow()
if self.produce < self.maxproduce then
self.produce = self.produce + 1
if self.ongrowfn then
self.ongrowfn(self.inst, self.produce)
end
if self.produce < self.maxproduce then
self:StartGrowing()
else
self:StopGrowing()
end
end
end

是一个非常简单的逻辑,所以对于这个接口是怎么运行的就大概明白了。

在前面的inst.components.harvestable:SetUp("pondfish", 3, nil, onharvest, updatelevel)内第三个时间参数是nil所以不会长,但是之前没改mod文件之前有蜜蜂会去采蜜,然后就会有鱼了。

但是在harvest部分还有第三行inst:ListenForEvent("childgoinghome", onchildgoinghome),意思似乎是添加一个Listener来监听孩子回家,然后第二个参数好像在哪里见过的方法,遂在这个mod的代码内搜索:

1
2
3
4
5
6
7
local function onchildgoinghome(inst, data)
if data.child and data.child.components.pollinator and data.child.components.pollinator:HasCollectedEnough() then
if inst.components.harvestable then
inst.components.harvestable:Grow()
end
end
end

哦豁,他直接在这里调用了harvestableGrow方法。所以意思是只要蜜蜂去采蜜了,那么就能有东西生成了,但是现在的问题是它根本就不产生蜜蜂。

所以猜测问题有可能是在生蜜蜂那里

1
2
3
4
5
6
7
8
9
inst:AddComponent("childspawner")
inst.components.childspawner.childname = "bee"
inst.components.childspawner:SetRegenPeriod(TUNING.BEEBOX_REGEN_TIME)
inst.components.childspawner:SetSpawnPeriod(TUNING.BEEBOX_RELEASE_TIME)
inst.components.childspawner:SetMaxChildren(TUNING.BEEBOX_BEES)

if TheWorld.state.isday and TheWorld.state.issummer then
inst.components.childspawner:StartSpawning()
end

嗯。。?这里怎么混进了一个判断语句?看语义是如果是在夏天的白天就开始生动物

感觉应该还有别的地方有这个StartSpawning,不然在其它季节建造的不就炸了。然后搜索StartSpawning,找到了这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local function OnIsDay(inst, isday)
if not isday then
if inst.components.harvestable and inst.components.harvestable.growtime then
inst.components.harvestable:StopGrowing()
end
if inst.components.childspawner then
inst.components.childspawner:StopSpawning()
end
elseif not TheWorld.state.iswinter then
if inst.components.harvestable and inst.components.harvestable.growtime then
inst.components.harvestable:StartGrowing()
end
if inst.components.childspawner then
inst.components.childspawner:StartSpawning()
end
end
end

逻辑有些混乱,找找在哪里调用了这个方法吧

1
inst:WatchWorldState("isday", OnIsDay)

感觉又是一个Listener,或者说Watcher?那么大概就明白了? 每个白天的开始都会执行一次这个方法?

然后在这个方法里面控制了怎么产生产物。

除此之外也有onsleepstopsleep的方法在里面控制了通过harvestable产生产品,并且在构造方法内把这两个方法绑定到了entitysleepentitywake两个Listener上,然而这两个Listener什么时候触发我也完全不知道。

剩下的东西明天再写,咕咕咕

饥荒联机版mod开发摸鱼(一)

今日起床较晚,本来计划着今天的每日一摸应该去搞一搞es6Proxy

并且也确实看了一会儿觉得挺好玩的,但是还没有摸完就有人提到了昨天玩饥荒遇到的事情

我们装了一个鱼塘的mod,然后那个作者上一次更新是好几年前,然后近期饥荒更新了一个部分就是原来钓鱼获得的死鱼现在是活鱼并且可以称重。

但是呢,那个鱼塘他的合成配方还是那个死鱼,所以就不能合成了。。。

然后我在游戏的scripts目录找了半天并没有找到淡水鱼这个prefabs

然后我就寻思着说不定fish即可以表示死鱼也能表示活鱼呢。。然后我就以这个错误的方向去找了很久的证据,然后脸被打的啪啪响。

其实一开始我会这么想纯粹是因为看到了一个meats.lua,也就是肉,在这个文件里面定义了很多的肉类,然后由于这个游戏的代码结构并不是很熟悉,所以我就踩了一个坑(x

找到正确的方向是因为突然灵光一闪,根据饥荒的烹饪系统,每个食物都会有不同的属性,比如鱼是1个单位的鱼,1个单位的肉。然后我找到了scripts根目录下的cooking.lua文件,这个文件里面定义了各个食物的烹饪属性,然后我翻到了fish发现它只有一个定义。然后人都傻了。因为有一个叫Show Me的mod可以在游戏内显示食物的各个烹饪属性,然后之前在游戏内看到了活鱼是0.5个单位的鱼和0.5个单位的肉。

不对,然后我就继续在cooking.lua找了一会儿,看到了一个肉和鱼都是0.5单位的,然后看到了一个prefabpondfish。然后。。莫非就是它了?这就是我找了一个下午的它?

在游戏内开控制台

1
c_give('pondfish')

啊!是他,一条活生生的鱼。

感觉干了一下午的无用功,看了挺多代码的。

收获大概就是大概明白了饥荒这个游戏scripts目录下面的一些文件的作用,以及更多的关于lua的语言特性,常用写法什么的,跟我之前写手游挂机脚本用的那些lua有比较大的区别。

可以看到在prefabs文件夹下的很多东西都会有onXxxxfunction,里面大致是一些触发某事件的行为,但是我并没有找到调用这些事件的地方,或许可以看看actions.lua这个文件,当然还有更多的可能需要找。

然后就是看了半天并没有找到程序的入口,这让人十分难受。也就是所谓的main函数,我的想法就是他应该会在一个很明显的位置,然后刚刚找到pondfish这个东西突然发现我看了这么久的代码还不知道到处可以看到的inst到底是个什么意思,遂百度之:instance指代的是某个物品或者单位的实例。

然后发现每个东西的下面总会有一个fn或者commonfn方法,看起来特别像扮演构造方法的角色,然后第一行就是local inst = CreateEntity()随在CreateEntity上按下了转到定义,然后一小会儿之后vscode带我到了一个叫mainfunctions.lua的文件里面,好家伙,里面有好多我平时看到的奇奇怪怪的函数。

然后最关键的是在这个文件旁边我还看到了一个main.lua,联想到c语言的int main(),再想到java的public static void main(String[] args),这莫非就是。。遂点开

哦豁,看起来很像这个东西了啊!

里面有好多的require(),看起来引用了很多的东西。

但是开头也有很多没预先定义的方法,所以最开始的入口是不是这个还真的不是很清楚,可能需要明天继续看

不得不说,慢慢的摸透一个程序的运行机制真的很爽(雾

然后这可能是真的每日摸鱼吧(x

然后又回到最初的起点,要让现在的版本可以做出那个鱼塘应该只需要吧AddRecipe()方法内的参数的fish换成pondfish就好了

不过要考虑到在新的版本这个mod还能不能变得好玩,那么就要让他的生成物也变成现在的版本的东西。

或许可以考虑以后可以业余写一写饥荒的mod,23333

其实游戏开发者好像也为modder留下了很多的余地,虽然这可能也是解释型的脚本语言不可避免地吧。

然后就是这样了,前面提到的目标留到以后实现。。嗯

我寻思着我应该会再有一篇文章叫做饥荒联机版mod开发摸鱼(二)什么的吧,2333