Giter Site home page Giter Site logo

personalblog's Introduction

Hi👋 I'm Nealyang

  • 🔭 I’m currently working on Alibaba group
  • 🌱 I’m currently learning React,JavaScript

I think these code repositories are okay...

📊 some stats

📊 Weekly development breakdown

TypeScript   9 hrs 34 mins   ███████████████▓░░░░░░░░░   62.97 % 
JavaScript   5 hrs 13 mins   ████████▓░░░░░░░░░░░░░░░░   34.36 % 
SCSS         15 mins         ▒░░░░░░░░░░░░░░░░░░░░░░░░   01.71 % 
HTML         7 mins          ▒░░░░░░░░░░░░░░░░░░░░░░░░   00.85 % 

personalblog's People

Contributors

deckedeng avatar nealyang avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

personalblog's Issues

【THE LAST TIME】彻底吃透 JavaScript 执行机制

前言

The last time, I have learned

【THE LAST TIME】一直是我想写的一个系列,旨在厚积薄发,重温前端。

也是给自己的查缺补漏和技术分享。

欢迎大家多多评论指点吐槽。

系列文章均首发于公众号【全栈前端精选】,笔者文章集合详见Nealyang/personalBlog。目录皆为暂定

执行 & 运行

首先我们需要声明下,JavaScript 的执行和运行是两个不同概念的,执行,一般依赖于环境,比如 node、浏览器、Ringo 等, JavaScript 在不同环境下的执行机制可能并不相同。而今天我们要讨论的 Event Loop 就是 JavaScript 的一种执行方式。所以下文我们还会梳理 node 的执行方式。而运行呢,是指JavaScript 的解析引擎。这是统一的。

关于 JavaScript

此篇文章中,这个小标题下,我们只需要牢记一句话: JavaScript 是单线程语言 ,无论HTML5 里面 Web-Worker 还是 node 里面的cluster都是“纸老虎”,而且 cluster 还是进程管理相关。这里读者注意区分:进程和线程。

既然 JavaScript 是单线程语言,那么就会存在一个问题,所有的代码都得一句一句的来执行。就像我们在食堂排队打饭,必须一个一个排队点菜结账。那些没有排到的,就得等着~

概念梳理

在详解执行机制之前,先梳理一下 JavaScript 的一些基本概念,方便后面我们说到的时候大伙儿心里有个印象和大概的轮廓。

事件循环(Event Loop)

什么是 Event Loop?

其实这个概念还是比较模糊的,因为他必须得结合着运行机制来解释。

JavaScript 有一个主线程 main thread,和调用栈 call-stack 也称之为执行栈。所有的任务都会放到调用栈中等待主线程来执行。

暂且,我们先理解为上图的大圈圈就是 Event Loop 吧!并且,这个圈圈,一直在转圈圈~ 也就是说,JavaScriptEvent Loop 是伴随着整个源码文件生命周期的,只要当前 JavaScript 在运行中,内部的这个循环就会不断地循环下去,去寻找 queue 里面能执行的 task

任务队列(task queue)

task,就是任务的意思,我们这里理解为每一个语句就是一个任务

console.log(1);
console.log(2);

如上语句,其实就是就可以理解为两个 task

queue 呢,就是FIFO的队列!

所以 Task Queue 就是承载任务的队列。而 JavaScriptEvent Loop 就是会不断地过来找这个 queue,问有没有 task 可以运行运行。

同步任务(SyncTask)、异步任务(AsyncTask)

同步任务说白了就是主线程来执行的时候立即就能执行的代码,比如:

console.log('this is THE LAST TIME');
console.log('Nealyang');

代码在执行到上述 console 的时候,就会立即在控制台上打印相应结果。

而所谓的异步任务就是主线程执行到这个 task 的时候,“唉!你等会,我现在先不执行,等我 xxx 完了以后我再来等你执行” 注意上述我说的是等你来执行。

说白了,异步任务就是你先去执行别的 task,等我这 xxx 完之后再往 Task Queue 里面塞一个 task 的同步任务来等待被执行

setTimeout(()=>{
  console.log(2)
});
console.log(1);

如上述代码,setTimeout 就是一个异步任务,主线程去执行的时候遇到 setTimeout 发现是一个异步任务,就先注册了一个异步的回调,然后接着执行下面的语句console.log(1),等上面的异步任务等待的时间到了以后,在执行console.log(2)。具体的执行机制会在后面剖析。

  • 主线程自上而下执行所有代码
  • 同步任务直接进入到主线程被执行,而异步任务则进入到 Event Table 并注册相对应的回调函数
  • 异步任务完成后,Event Table 会将这个函数移入 Event Queue
  • 主线程任务执行完了以后,会从Event Queue中读取任务,进入到主线程去执行。
  • 循环如上

上述动作不断循环,就是我们所说的事件循环(Event Loop)。

小试牛刀

ajax({
    url:www.Nealyang.com,
    data:prams,
    success:() => {
        console.log('请求成功!');
    },
    error:()=>{
        console.log('请求失败~');
    }
})
console.log('这是一个同步任务');
  • ajax 请求首先进入到 Event Table ,分别注册了onErroronSuccess回调函数。
  • 主线程执行同步任务:console.log('这是一个同步任务');
  • 主线程任务执行完毕,看Event Queue是否有待执行的 task,这里是不断地检查,只要主线程的task queue没有任务执行了,主线程就一直在这等着
  • ajax 执行完毕,将回调函数pushEvent Queue。(步骤 3、4 没有先后顺序而言)
  • 主线程“终于”等到了Event Queue里有 task可以执行了,执行对应的回调任务。
  • 如此往复。

宏任务(MacroTask)、微任务(MicroTask)

JavaScript 的任务不仅仅分为同步任务和异步任务,同时从另一个维度,也分为了宏任务(MacroTask)和微任务(MicroTask)。

先说说 MacroTask,所有的同步任务代码都是MacroTask(这么说其实不是很严谨,下面解释),setTimeoutsetIntervalI/OUI Rendering 等都是宏任务。

MicroTask,为什么说上述不严谨我却还是强调所有的同步任务都是 MacroTask 呢,因为我们仅仅需要记住几个 MicroTask 即可,排除法!别的都是 MacroTaskMicroTask 包括:Process.nextTickPromise.then catch finally(注意我不是说 Promise)、MutationObserver

浏览器环境下的 Event Loop

当我们梳理完哪些是 MicroTask ,除了那些别的都是 MacroTask 后,哪些是同步任务,哪些又是异步任务后,这里就应该彻底的梳理下JavaScript 的执行机制了。

如开篇说到的,执行和运行是不同的,执行要区分环境。所以这里我们将 Event Loop 的介绍分为浏览器和 Node 两个环境下。

先放图镇楼!如果你已经理解了这张图的意思,那么恭喜你,你完全可以直接阅读 Node 环境下的 Event Loop 章节了!

setTimeout、setInterval

setTimeout

setTimeout 就是等多长时间来执行这个回调函数。setInterval 就是每隔多长时间来执行这个回调。

let startTime = new Date().getTime();

setTimeout(()=>{
  console.log(new Date().getTime()-startTime);
},1000);

如上代码,顾名思义,就是等 1s 后再去执行 console。放到浏览器下去执行,OK,如你所愿就是如此。

但是这次我们在探讨 JavaScript 的执行机制,所以这里我们得探讨下如下代码:

let startTime = new Date().getTime();

console.log({startTime})

setTimeout(()=>{
  console.log(`开始执行回调的相隔时差:${new Date().getTime()-startTime}`);
},1000);

for(let i = 0;i<40000;i++){
  console.log(1)
}

如上运行,setTimeout 的回调函数等到 4.7s 以后才执行!而这时候,我们把 setTimeout 的 1s 延迟给删了:

let startTime = new Date().getTime();

console.log({startTime})

setTimeout(()=>{
  console.log(`开始执行回调的相隔时差:${new Date().getTime()-startTime}`);
},0);

for(let i = 0;i<40000;i++){
  console.log(1)
}

结果依然是等到 4.7s 后才执行setTimeout 的回调。貌似 setTimeout 后面的延迟并没有产生任何效果!

其实这么说,又应该回到上面的那张 JavaScript 执行的流程图了。

setTimeout这里就是简单的异步,我们通过上面的图来分析上述代码的一步一步执行情况

  • 首先 JavaScript 自上而下执行代码
  • 遇到遇到赋值语句、以及第一个 console.log({startTime}) 分别作为一个 task,压入到立即执行栈中被执行。
  • 遇到 setTImeout 是一个异步任务,则注册相应回调函数。(异步函数告诉你,js 你先别急,等 1s 后我再将回调函数:console.log(xxx)放到 Task Queue 中)
  • OK,这时候 JavaScript 则接着往下走,遇到了 40000 个 for 循环的 task,没办法,1s 后都还没执行完。其实这个时候上述的回调已经在Task Queue 中了。
  • 等所有的立即执行栈中的 task 都执行完了,在回头看 Task Queue 中的任务,发现异步的回调 task 已经在里面了,所以接着执行。

打个比方

其实上述的不仅仅是 timeout,而是任何异步,比如网络请求等。

就好比,我六点钟下班了,可以安排下自己的活动了!

然后收拾电脑(同步任务)、收拾书包(同步任务)、给女朋友打电话说出来吃饭吧(必然是异步任务),然后女朋友说你等会,我先化个妆,等我画好了call你。

那我不能干等着呀,就接着做别的事情,比如那我就在改个 bug 吧,你好了通知我。结果等她一个小时后说我化好妆了,我们出去吃饭吧。不行!我 bug 还没有解决掉呢?你等会。。。。其实这个时候你的一小时化妆还是 5 分钟化妆都已经毫无意义了。。。因为哥哥这会没空~~

如果我 bug 在半个小时就解决完了,没别的任务需要执行了,那么就在这等着呀!必须等着!随时待命!。然后女朋友来电话了,我化完妆了,我们出去吃饭吧,那么刚好,我们在你的完成了请求或者 timeout 时间到了后我刚好闲着,那么我必须立即执行了。

setInterval

说完了 setTimeout,当然不能错过他的孪生兄弟:setInterval。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入 Task Queue,如果前面的任务耗时太久,那么同样需要等待。

这里需要说的是,对于 setInterval(fn,ms) 来说,我们制定没 xx ms执行一次 fn,其实是没 xx ms,会有一个fn 进入到 Task Queue 中。一旦 setInterval 的回调函数fn执行时间超过了xx ms,那么就完全看不出来有时间间隔了。 仔细回味回味,是不是那么回事?

Promise

关于 Promise 的用法,这里就不过过多介绍了,后面会在写《【THE LAST TIME】彻底吃透 JavaScript 异步》 一文的时候详细介绍。这里我们只说 JavaScript 的执行机制。

如上所说,promise.thencatchfinally 是属于 MicroTask。这里主要是异步的区分。展开说明之前,我们结合上述说的,再来“扭曲”梳理一下。

为了避免初学者这时候脑子有点混乱,我们暂时忘掉 JavaScript 异步任务! 我们暂且称之为待会再执行的同步任务。

有了如上约束后,我们可以说,JavaScript 从一开始就自上而下的执行每一个语句(Task),这时候只能遇到立马就要执行的任务和待会再执行的任务。对于那待会再执行的任务等到能执行了,也不会立即执行,你得等js 执行完这一趟才行

再打个比方

就像做公交车一样,公交车不等人呀,公交车路线上有人就会停(农村公交!么得站牌),但是等公交车来,你跟司机说,我肚子疼要拉x~这时候公交不会等你。你只能拉完以后等公交下一趟再来(大山里!一个路线就一趟车)。

OK!你拉完了。。。等公交,公交也很快到了!但是,你不能立马上车,因为这时候前面有个孕妇!有个老人!还有熊孩子,你必须得让他们先上车,然后你才能上车!

而这些 孕妇、老人、熊孩子所组成的就是传说中的 MicroTask Queue,而且,就在你和你的同事、朋友就必须在他们后面上车。

这里我们没有异步的概念,只有同样的一次循环回来,有了两种队伍,一种优先上车的队伍叫做MicroTask Queue,而你和你的同事这帮壮汉组成的队伍就是宏队伍(MacroTask Queue)。

一句话理解:一次事件循环回来后,开始去执行 Task Queue 中的 task,但是这里的 task优先级。所以优先执行 MicroTask Queue 中的 task
,执行完后在执行MacroTask Queue 中的 task

小试牛刀

理论都扯完了,也不知道你懂没懂。来,期中考试了!

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

没必要搞个 setTimeout 有加个 Promise,Promise 里面再整个 setTimeout 的例子。因为只要上面代码你懂了,无非就是公交再来一趟而已!

如果说了这么多,还是没能理解上图,那么公众号内回复【1】,手摸手指导!

Node 环境下的 Event Loop

Node中的Event Loop是基于libuv实现的,而libuv是 Node 的新跨平台抽象层,libuv使用异步,事件驱动的编程方式,核心是提供i/o的事件循环和异步回调。libuvAPI包含有时间,非阻塞的网络,异步文件操作,子进程等等。

Event Loop就是在libuv中实现的。所以关于 Node 的 Event Loop学习,有两个官方途径可以学习:

在学习 Node 环境下的 Event Loop 之前呢,我们首先要明确执行环境,Node 和浏览器的Event Loop是两个有明确区分的事物,不能混为一谈。nodejs的event是基于libuv,而浏览器的event loop则在html5的规范中明确定义。

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

Node 的 Event Loop 分为 6 个阶段:

  • timers:执行setTimeout()setInterval()中到期的callback。
  • pending callback: 上一轮循环中有少数的I/O callback会被延迟到这一轮的这一阶段执行
  • idle, prepare:仅内部使用
  • poll: 最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段
  • check: 执行setImmediate的callback
  • close callbacks: 执行close事件的callback,例如socket.on('close'[,fn])http.server.on('close, fn)

上面六个阶段都不包括 process.nextTick()(下文会介绍)

整体的执行机制如上图所示,下面我们具体展开每一个阶段的说明

timers 阶段

timers 阶段会执行 setTimeoutsetInterval 回调,并且是由 poll 阶段控制的。

在 timers 阶段其实使用一个最小堆而不是队列来保存所有的元素,其实也可以理解,因为timeout的callback是按照超时时间的顺序来调用的,并不是先进先出的队列逻辑)。而为什么 timer 阶段在第一个执行阶梯上其实也不难理解。在 Node 中定时器指定的时间也是不准确的,而这样,就能尽可能的准确了,让其回调函数尽快执行。

以下是官网给出的例子:

const fs = require('fs');

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);

// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
  const startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});

当进入事件循环时,它有一个空队列(fs.readFile()尚未完成),因此定时器将等待剩余毫秒数,当到达95ms时,fs.readFile()完成读取文件并且其完成需要10毫秒的回调被添加到轮询队列并执行。

当回调结束时,队列中不再有回调,因此事件循环将看到已达到最快定时器的阈值,然后回到timers阶段以执行定时器的回调。
在此示例中,您将看到正在调度的计时器与正在执行的回调之间的总延迟将为105毫秒。

pending callbacks 阶段

pending callbacks 阶段其实是 I/O 的 callbacks 阶段。比如一些 TCP 的 error 回调等。

举个栗子:如果TCP socket ECONNREFUSED在尝试connectreceives,则某些* nix系统希望等待报告错误。 这将在pending callbacks阶段执行。

poll 阶段

poll 阶段主要有两个功能:

  • 执行 I/O 回调
  • 处理 poll 队列(poll queue)中的事件

当时Event Loop 进入到 poll 阶段并且 timers 阶段没有任何可执行的 task 的时候(也就是没有定时器回调),将会有以下两种情况

  • 如果 poll queue 非空,则 Event Loop就会执行他们,知道为空或者达到system-dependent(系统相关限制)
  • 如果 poll queue 为空,则会发生以下一种情况
    • 如果setImmediate()有回调需要执行,则会立即进入到 check 阶段
    • 相反,如果没有setImmediate()需要执行,则 poll 阶段将等待 callback 被添加到队列中再立即执行,这也是为什么我们说 poll 阶段可能会阻塞的原因。

一旦 poll queue 为空,Event Loop就回去检查timer 阶段的任务。如果有的话,则会回到 timer 阶段执行回调。

check 阶段

check 阶段在 poll 阶段之后,setImmediate()的回调会被加入check队列中,他是一个使用libuv API 的特殊的计数器。

通常在代码执行的时候,Event Loop 最终会到达 poll 阶段,然后等待传入的链接或者请求等,但是如果已经指定了setImmediate()并且这时候 poll 阶段已经空闲的时候,则 poll 阶段将会被中止然后开始 check 阶段的执行。

close callbacks 阶段

如果一个 socket 或者事件处理函数突然关闭/中断(比如:socket.destroy()),则这个阶段就会发生 close 的回调执行。否则他会通过 process.nextTick() 发出。

setImmediate() vs setTimeout()

setImmediate()setTimeout()非常的相似,区别取决于谁调用了它。

  • setImmediate在 poll 阶段后执行,即check 阶段
  • setTimeout 在 poll 空闲时且设定时间到达的时候执行,在 timer 阶段

计时器的执行顺序将根据调用它们的上下文而有所不同。 如果两者都是从主模块中调用的,则时序将受到进程性能的限制。

例如,如果我们运行以下不在I / O周期(即主模块)内的脚本,则两个计时器的执行顺序是不确定的,因为它受进程性能的约束:

// timeout_vs_immediate.js
setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

如果在一个I/O 周期内移动这两个调用,则始终首先执行立即回调:

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout

所以与setTimeout()相比,使用setImmediate()的主要优点是,如果在I / O周期内安排了任何计时器,则setImmediate()将始终在任何计时器之前执行,而与存在多少计时器无关。

nextTick queue

可能你已经注意到process.nextTick()并未显示在图中,即使它是异步API的一部分。 所以他拥有一个自己的队列:nextTickQueue

这是因为process.nextTick()从技术上讲不是Event Loop的一部分。 相反,无论当前事件循环的当前阶段如何,都将在当前操作完成之后处理nextTickQueue

如果存在 nextTickQueue,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

setTimeout(() => {
 console.log('timer1')
 Promise.resolve().then(function() {
   console.log('promise1')
 })
}, 0)
process.nextTick(() => {
 console.log('nextTick')
 process.nextTick(() => {
   console.log('nextTick')
   process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
     })
   })
 })
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1

process.nextTick() vs setImmediate()

从使用者角度而言,这两个名称非常的容易让人感觉到困惑。

  • process.nextTick()在同一阶段立即触发
  • setImmediate()在事件循环的以下迭代或“tick”中触发

貌似这两个名称应该呼唤下!的确~官方也这么认为。但是他们说这是历史包袱,已经不会更改了。

这里还是建议大家尽可能使用setImmediate。因为更加的让程序可控容易推理。

至于为什么还是需要 process.nextTick,存在即合理。这里建议大家阅读官方文档:why-use-process-nexttick

Node与浏览器的 Event Loop 差异

一句话总结其中:浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。

上图来自浪里行舟

最后

来~期末考试了

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

评论区留下你的答案吧~~老铁!

参考文献

FlutterGo 后台代码知识点提炼:midway+Typescript+mysql(sequelize)

前言

关于 FlutterGo 或许不用太多介绍了。

如果有第一次听说的小伙伴,可以移步FlutterGo官网查看下简单介绍.

FlutterGo 在这次迭代中有了不少的更新,笔者在此次的更新中,负责开发后端以及对应的客户端部分。这里简单介绍下关于 FlutterGo 后端代码中几个功能模块的实现。

总体来说,FlutterGo 后端并不复杂。此文中大概介绍以下几点功能(接口)的实现:

  • FlutterGo 登陆功能
  • 组件获取功能
  • 收藏功能
  • 建议反馈功能

环境信息

阿里云 ECS 云服务器

Linux iz2ze3gw3ipdpbha0mstybz 3.10.0-957.21.3.el7.x86_64 #1 SMP Tue Jun 18 16:35:19 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

mysql :mysql Ver 8.0.16 for Linux on x86_64 (MySQL Community Server - GPL)

node:v12.5.0

开发语言:midway + typescript + mysql

代码结构:

src
├─ app
│    ├─ class 定义表结构
│    │    ├─ app_config.ts 
│    │    ├─ cat.ts
│    │    ├─ collection.ts
│    │    ├─ user.ts
│    │    ├─ user_collection.ts
│    │    └─ widget.ts
│    ├─ constants 常量
│    │    └─ index.ts
│    ├─ controller 
│    │    ├─ app_config.ts
│    │    ├─ auth.ts
│    │    ├─ auth_collection.ts
│    │    ├─ cat_widget.ts
│    │    ├─ home.ts
│    │    ├─ user.ts
│    │    └─ user_setting.ts
│    ├─ middleware 中间件
│    │    └─ auth_middleware.ts
│    ├─ model
│    │    ├─ app_config.ts
│    │    ├─ cat.ts
│    │    ├─ collection.ts
│    │    ├─ db.ts
│    │    ├─ user.ts
│    │    ├─ user_collection.ts
│    │    └─ widget.ts
│    ├─ public
│    │    └─ README.md
│    ├─ service
│    │    ├─ app_config.ts
│    │    ├─ cat.ts
│    │    ├─ collection.ts
│    │    ├─ user.ts
│    │    ├─ user_collection.ts
│    │    ├─ user_setting.ts
│    │    └─ widget.ts
│    └─ util 工具集
│           └─ index.ts
├─ config 应用的配置信息
│    ├─ config.default.ts
│    ├─ config.local.ts
│    ├─ config.prod.ts
│    └─ plugin.ts
└─ interface.ts

登陆功能

首先在class/user.ts中定义一个 user 表结构,大概需要的字段以及在 interface.ts 中声明相关接口。这里是 midwayts 的基础配置,就不展开介绍了。

FlutterGo 提供了两种登陆方式:

  • 用户名、密码登陆
  • GitHubOAuth 认证

因为是手机客户端的 GitHubOauth 认证,所以这里其实是有一些坑的,后面再说。这里我们先从简单的开始说起

用户名/密码登陆

因为我们使用 github 的用户名/密码登陆方式,所以这里需要罗列下 github 的 api:developer.github.com/v3/auth/,

文档中的核心部分:curl -u username https://api.github.com/user (大家可以自行在 terminal 上测试),回车输入密码即可。所以这里我们完全可以在拿到用户输入的用户名和密码后进行 githu 的认证。

关于 midway 的基本用法,这里也不再赘述了。整个过程还是非常简单清晰的,如下图:

相关代码实现(相关信息已脱敏:xxx):

service部分

    //获取 userModel
    @inject()
    userModel
    
    // 获取 github 配置信息
    @config('githubConfig')
    GITHUB_CONFIG;

    //获取请求上下文
    @inject()
    ctx;
    //githubAuth 认证
    async githubAuth(username: string, password: string, ctx): Promise<any> {
        return await ctx.curl(GITHUB_OAUTH_API, {
            type: 'GET',
            dataType: 'json',
            url: GITHUB_OAUTH_API,
            headers: {
                'Authorization': ctx.session.xxx
            }
        });
    }
    // 查找用户 
    async find(options: IUserOptions): Promise<IUserResult> {
        const result = await this.userModel.findOne(
            {
                attributes: ['xx', 'xx', 'xx', 'xx', 'xx', "xx"],//相关信息脱敏
                where: { username: options.username, password: options.password }
            })
            .then(userModel => {
                if (userModel) {
                    return userModel.get({ plain: true });
                }
                return userModel;
            });
        return result;
    }
    // 通过 URLName 查找用户
    async findByUrlName(urlName: string): Promise<IUserResult> {
        return await this.userModel.findOne(
            {
                attributes: ['xxx', 'xxx', 'xxx', 'xxx', 'xxx', "xxx"],
                where: { url_name: urlName }
            }
        ).then(userModel => {
            if (userModel) {
                return userModel.get({ plain: true });
            }
            return userModel;
        });
    }
    // 创建用户
    async create(options: IUser): Promise<any> {
        const result = await this.userModel.create(options);
        return result;
    }
    
    // 更新用户信息
    async update(id: number, options: IUserOptions): Promise<any> {
        return await this.userModel.update(
            {
                username: options.username,
                password: options.password
            },
            {
                where: { id },
                plain: true
            }
        ).then(([result]) => {
            return result;
        });
    }

controller

    // inject 获取 service 和加密字符串
    @inject('userService')
    service: IUserService

    @config('random_encrypt')
    RANDOM_STR;
流程图中逻辑的代码实现

GitHubOAuth 认证

这里有坑!我回头介绍

githubOAuth 认证就是我们常说的 github app 了,这里我直接了当的丢文档:creating-a-github-app


笔者还是觉得文档类的无需介绍

当然,我这里肯定都建好了,然后把一些基本信息都写到 server 端的配置中

还是按照上面的套路,咱们先介绍流程。然后在说坑在哪。

客户端部分

客户端部分的代码就相当简单了,新开 webView ,直接跳转到 github.com/login/oauth/authorize 带上 client_id即可。

server 端

整体流程如上,部分代码展示:

service

    //获取 github access_token
    async getOAuthToken(code: string): Promise<any> {
        return await this.ctx.curl(GITHUB_TOKEN_URL, {
            type: "POST",
            dataType: "json",
            data: {
                code,
                client_id: this.GITHUB_CONFIG.client_id,
                client_secret: this.GITHUB_CONFIG.client_secret
            }
        });
    }

controller代码逻辑就是调用 service 中的数据来走上面流程图中的信息。

OAuth 中的坑

其实,github app 的认证方式非常适用于浏览器环境下,但是在 flutter 中,由于我们是新开启的 webView 来请求的 github 登陆地址。当我们后端成功返回的时候,无法通知到 Flutter 层。就导致我自己的 Flutter 中 dart 写的代码,无法拿到接口的返回。

中间脑暴了很多解决办法,最终在查阅 flutter_webview_plugin 的 API 里面找了个好的方法:onUrlChanged

简而言之就是,Flutter 客户端部分新开一个 webView去请求 github.com/login,github.com/login检查 client_id 后会带着code 等乱七八糟的东西来到后端,后端校验成功后,redirect Flutter 新开的 webView,然后flutter_webview_plugin去监听页面 url 的变化。发送相关 event ,让Flutter 去 destroy 当前 webVIew,处理剩余逻辑。

Flutter 部分代码

//定义相关 OAuth event
class UserGithubOAuthEvent{
  final String loginName;
  final String token;
  final bool isSuccess;
  UserGithubOAuthEvent(this.loginName,this.token,this.isSuccess);
}

webView page:

    //在 initState 中监听 url 变化,并emit event
    flutterWebviewPlugin.onUrlChanged.listen((String url) {
      if (url.indexOf('loginSuccess') > -1) {
        String urlQuery = url.substring(url.indexOf('?') + 1);
        String loginName, token;
        List<String> queryList = urlQuery.split('&');
        for (int i = 0; i < queryList.length; i++) {
          String queryNote = queryList[i];
          int eqIndex = queryNote.indexOf('=');
          if (queryNote.substring(0, eqIndex) == 'loginName') {
            loginName = queryNote.substring(eqIndex + 1);
          }
          if (queryNote.substring(0, eqIndex) == 'accessToken') {
            token = queryNote.substring(eqIndex + 1);
          }
        }
        if (ApplicationEvent.event != null) {
          ApplicationEvent.event
              .fire(UserGithubOAuthEvent(loginName, token, true));
        }
        print('ready close');

        flutterWebviewPlugin.close();
        // 验证成功
      } else if (url.indexOf('${Api.BASE_URL}loginFail') == 0) {
        // 验证失败
        if (ApplicationEvent.event != null) {
          ApplicationEvent.event.fire(UserGithubOAuthEvent('', '', true));
        }
        flutterWebviewPlugin.close();
      }
    });

login page:

    //event 的监听、页面跳转以及提醒信息的处理
    ApplicationEvent.event.on<UserGithubOAuthEvent>().listen((event) {
      if (event.isSuccess == true) {
        //  oAuth 认证成功
        if (this.mounted) {
          setState(() {
            isLoading = true;
          });
        }
        DataUtils.getUserInfo(
                {'loginName': event.loginName, 'token': event.token})
            .then((result) {
          setState(() {
            isLoading = false;
          });
          Navigator.of(context).pushAndRemoveUntil(
              MaterialPageRoute(builder: (context) => AppPage(result)),
              (route) => route == null);
        }).catchError((onError) {
          print('获取身份信息 error:::$onError');
          setState(() {
            isLoading = false;
          });
        });
      } else {
        Fluttertoast.showToast(
            msg: '验证失败',
            toastLength: Toast.LENGTH_SHORT,
            gravity: ToastGravity.CENTER,
            timeInSecForIos: 1,
            backgroundColor: Theme.of(context).primaryColor,
            textColor: Colors.white,
            fontSize: 16.0);
      }
    });

组件树获取

表结构

在聊接口实现的之前,我们先了解下,关于组件,我们的表机构设计大概是什么样子的。

FlutterGO 下面 widget tab很多分类,分类点进去还是分类,再点击去是组件,组件点进去是详情页。


上图模块点进去就是组件 widget


上图是 widget,点进去是详情页

所以这里我们需要两张表来记录他们的关系:cat(category)和 widget 表。

cat 表中我们每行数据会有一个 parent_id 字段,所以表内存在父子关系,而 widget 表中的每一行数据的 parent_id 字段的值必然是 cat 表中的最后一层。比如 Checkbox widgetparent_id 的值就是 cat 表中 Button 的 id。

需求实现

在登陆的时候,我们希望能获取所有的组件树,需求方要求结构如下:

[
   {
    "name": "Element",
      "type": "root",
      "child": [
        {
          "name": "Form",
            "type": "group",
            "child": [
              {
                "name": "input",
                  "type": "page",
                  "display": "old",
                  "extends": {},
                  "router": "/components/Tab/Tab"
               },
               {
                "name": "input",
                  "type": "page",
                  "display": "standard",
                  "extends": {},
                  "pageId": "page1_hanxu_172ba42f_0520_401e_b568_ba7f7f6835e4"
               }
            ]
         }
      ],
   }
]

因为现在存在三方共建组件,而且我们详情页也较FlutterGo 1.0 版本有了很大改动,如今组件的详情页只有一个,内容全部靠 md 渲染,在 md 中写组件的 demo 实现。所以为了兼容旧版本的 widget,我们有 display 来区分,新旧 widget 分别通过 pageIdrouter 来跳转页面。

新建 widget 的 pageId 是通过FlutterGo 脚手架 goCli生成的

目前实现实际返回为:

{
    "success": true,
    "data": [
        {
            "id": "3",
            "name": "Element",
            "parentId": 0,
            "type": "root",
            "children": [
                {
                    "id": "6",
                    "name": "Form",
                    "parentId": 3,
                    "type": "category",
                    "children": [
                        {
                            "id": "9",
                            "name": "Input",
                            "parentId": 6,
                            "type": "category",
                            "children": [
                                {
                                    "id": "2",
                                    "name": "TextField",
                                    "parentId": "9",
                                    "type": "widget",
                                    "display": "old",
                                    "path": "/Element/Form/Input/TextField"
                                }
                            ]
                        },
                        {
                            "id": "12",
                            "name": "Text",
                            "parentId": 6,
                            "type": "category",
                            "children": [
                                {
                                    "id": "3",
                                    "name": "Text",
                                    "parentId": "12",
                                    "type": "widget",
                                    "display": "old",
                                    "path": "/Element/Form/Text/Text"
                                },
                                {
                                    "id": "4",
                                    "name": "RichText",
                                    "parentId": "12",
                                    "type": "widget",
                                    "display": "old",
                                    "path": "/Element/Form/Text/RichText"
                                }
                            ]
                        },
                        {
                            "id": "13",
                            "name": "Radio",
                            "parentId": 6,
                            "type": "category",
                            "children": [
                                {
                                    "id": "5",
                                    "name": "TestNealya",
                                    "parentId": "13",
                                    "type": "widget",
                                    "display": "standard",
                                    "pageId": "page1_hanxu_172ba42f_0520_401e_b568_ba7f7f6835e4"
                                }
                            ]
                        }
                    ]
                }
            ]
        }
        {
            "id": "5",
            "name": "Themes",
            "parentId": 0,
            "type": "root",
            "children": []
        }
    ]
}

简单示例,省去 99%数据

代码实现

其实这个接口也是非常简单的,就是个双循环遍历嘛,准确的说,有点类似深度优先遍历。直接看代码吧

获取所有 parentId 相同的 category (后面简称为 cat)

async getAllNodeByParentIds(parentId?: number) {
    if (!!!parentId) {
        parentId = 0;
    }

    return await this.catService.getCategoryByPId(parentId);
}

首字母转小写

firstLowerCase(str){
    return str[0].toLowerCase()+str.slice(1);
}

我们只要自己外部维护一个组件树,然后cat表中的读取到的每一个parent_id都是一个节点。当前 id 没有别的 cat 对应的 parent_id就说明它的下一级是“叶子” widget了,所以就从 widget 中查询即可。easy~

    //删除部分不用代码
   @get('/xxx')
    async getCateList(ctx) {
        const resultList: IReturnCateNode[] = [];
        let buidList = async (parentId: number, containerList: Partial<IReturnCateNode>[] | Partial<IReturnWidgetNode>[], path: string) => {
            let list: IReturnCateNode[] = await this.getAllNodeByParentIds(parentId);
            if (list.length > 0) {
                for (let i = 0; i < list.length; i++) {
                    let catNode: IReturnCateNode;
                    catNode = {
                        xxx:xxx
                    }
                    containerList.push(catNode);
                    await buidList(list[i].id, containerList[i].children, `${path}/${this.firstLowerCase(containerList[i].name)}`);
                }
            } else {
                // 没有 cat 表下 children,判断是否存在 widget
                const widgetResult = await this.widgetService.getWidgetByPId(parentId);
                if (widgetResult.length > 0) {
                    widgetResult.map((instance) => {
                        let tempWidgetNode: Partial<IReturnWidgetNode> = {};
                        tempWidgetNode.xxx = instance.xxx;
                        if (instance.display === 'old') {
                            tempWidgetNode.path = `${path}/${this.firstLowerCase(instance.name)}`;
                        } else {
                            tempWidgetNode.pageId = instance.pageId;
                        }
                        containerList.push(tempWidgetNode);
                    });
                } else {
                    return null;
                }

            }
        }
        await buidList(0, resultList, '');
        ctx.body = { success: true, data: resultList, status: 200 };
    }

彩蛋

FlutterGo 中有一个组件搜索功能,因为我们存储 widget 的时候,并没有强制带上该 widget的路由,这样也不合理(针对于旧组件),所以在widget表中搜索出来,还要像上述过程那样逆向搜索获取“旧”widgetrouter字段

我的个人代码实现大致如下:

    @get('/xxx')
    async searchWidget(ctx){
        let {name} = ctx.query;
        name = name.trim();
        if(name){
            let resultWidgetList = await this.widgetService.searchWidgetByStr(name);
            if(xxx){
                for(xxx){
                    if(xxx){
                        let flag = true;
                        xxx
                        while(xxx){
                            let catResult = xxx;
                            if(xxx){
                               xxx
                                if(xxx){
                                    flag = false;
                                }
                            }else{
                                flag = false;
                            }
                        }
                        resultWidgetList[i].path = path;
                    }
                }
                ctx.body={success:true,data:resultWidgetList,message:'查询成功'};
            }else{
                ctx.body={success:true,data:[],message:'查询成功'};
            }
        }else{
            ctx.body={success:false,data:[],message:'查询字段不能为空'};
        }
        
    }

求大神指教最简实现~🤓

收藏功能

收藏功能,必然是跟用户挂钩的。然后收藏的组件该如何跟用户挂钩呢?组件跟用户是多对多的关系。

这里我新建一个collection表来用作所有收藏过的组件。为什么不直接使用widget表呢,因为我个人不希望表太过于复杂,无用的字段太多,且功能不单一。

由于是收藏的组件和用户是多对多的关系,所以这里我们需要一个中间表user_collection来维护他两的关系,三者关系如下:

功能实现思路

  • 校验收藏

    • collection表中检查用户传入的组件信息,没有则为收藏、有则取出其在 collection 表中的 id
    • session 中获取用户的 id
    • collection_iduser_id 来检索user_collection表中是否有这个字段
  • 添加收藏

    • 获取用户传来的组件信息
    • findOrCrate的检索 collection表,并且返回一个 collection_id
    • 然后将 user_idcollection_id存入到 user_collection 表中(互不信任原则,校验下存在性)
  • 移除收藏

    • 步骤如上,拿到 collection 表中的 collection_id
    • 删除 user_collection 对应字段即可
  • 获取全部收藏

    • 检索 collection 表中所有 user_id 为当前用户的所有 collection_id
    • 通过拿到的collection_ids 来获取收藏的组件列表

部分代码实现

整体来说,思路还是非常清晰的。所以这里我们仅仅拿收藏和校验来展示下部分代码:

service层代码实现

    @inject()
    userCollectionModel;
        async add(params: IuserCollection): Promise<IuserCollection> {
        return await this.userCollectionModel.findOrCreate({
            where: {
                user_id: params.user_id, collection_id: params.collection_id
            }
        }).then(([model, created]) => {
            return model.get({ plain: true })
        })
    }

    async checkCollected(params: IuserCollection): Promise<boolean> {
        return await this.userCollectionModel.findAll({
            where: { user_id: params.user_id, collection_id: params.collection_id }
        }).then(instanceList => instanceList.length > 0);
    }

controller层代码实现

    @inject('collectionService')
    collectionService: ICollectionService;

    @inject()
    userCollectionService: IuserCollectionService

    @inject()
    ctx;
    
    // 校验组件是否收藏
    @post('/xxx')
    async checkCollected(ctx) {
        if (ctx.session.userInfo) {
            // 已登录
            const collectionId = await this.getCollectionId(ctx.request.body);
            const userCollection: IuserCollection = {
                user_id: this.ctx.session.userInfo.id,
                collection_id: collectionId
            }
            const hasCollected = await this.userCollectionService.checkCollected(userCollection);
            ctx.body={status:200,success:true,hasCollected};

        } else {
            ctx.body={status:200,success:true,hasCollected:false};
        }
    }
    
    async addCollection(requestBody): Promise<IuserCollection> {

        const collectionId = await this.getCollectionId(requestBody);

        const userCollection: IuserCollection = {
            user_id: this.ctx.session.userInfo.id,
            collection_id: collectionId
        }

        return await this.userCollectionService.add(userCollection);
    }

因为常要获取 collection 表中的 collection_id 字段,所以这里抽离出来作为公共方法

    async getCollectionId(requestBody): Promise<number> {
        const { url, type, name } = requestBody;
        const collectionOptions: ICollectionOptions = {
            url, type, name
        };
        const collectionResult: ICollection = await this.collectionService.findOrCreate(collectionOptions);
        return collectionResult.id;
    }

feedback 功能

feedback 功能就是直接可以在 FlutterGo 的个人设置中,发送 issue 到 Alibaba/flutter-go 下。这里主要也是调用 github 的提 issue 接口 api issues API

后端的代码实现非常简单,就是拿到数据,调用 github 的 api 即可

service

    @inject()
    ctx;

    async feedback(title: string, body: string): Promise<any> {
        return await this.ctx.curl(GIHTUB_ADD_ISSUE, {
            type: "POST",
            dataType: "json",
            headers: {
                'Authorization': this.ctx.session.headerAuth,
            },
            data: JSON.stringify({
                title,
                body,
            })
        });
    }

controller

    @inject('userSettingService')
    settingService: IUserSettingService;

    @inject()
    ctx;

    async feedback(title: string, body: string): Promise<any> {
        return await this.settingService.feedback(title, body);
    }

彩蛋

猜测可能会有人 FlutterGo 里面这个 feedback 是用的哪一个组件~这里介绍下

pubspec.yaml

  zefyr:
    path: ./zefyr

因为在开发的时候,flutter 更新了,导致zefyr 运行报错。当时也是提了 issue:chould not Launch FIle (写这篇文章的时候才看到回复)

但是当时由于功能开发要发布,等了好久没有zefyr作者的回复。就在本地修复了这个 bug,然后包就直接引入本地的包了。

共建计划

咳咳,敲黑板啦~~

Flutter 依旧在不断地更新,但仅凭我们几个 Flutter 爱好者在工作之余维护 FlutterGo 还是非常吃力的。所以这里,诚邀业界所有 Flutter 爱好者一起参与共建 FlutterGo!

此处再次感谢所有已经提交 pr 的小伙伴

共建说明

由于 Flutter 版本迭代速度较快,产生的内容较多, 而我们人力有限无法更加全面快速的支持Flutter Go的日常维护迭代, 如果您对flutter go的共建感兴趣, 欢迎您来参与本项目的共建.

凡是参与共建的成员. 我们会将您的头像与github个人地址收纳进我们的官方网站中.

共建方式

  1. 共建组件
  • 本次更新, 开放了 Widget 内容收录 的功能, 您需要通过 goCli 工具, 创建标准化组件,编写markdown代码。

  • 为了更好记录您的改动目的, 内容信息, 交流过程, 每一条PR都需要对应一条 Issue, 提交你发现的BUG或者想增加的新功能, 或者想要增加新的共建组件,

  • 首先选择你的issue在类型,然后通过 Pull Request 的形式将文章内容, api描述, 组件使用方法等加入进我们的Widget界面。

  1. 提交文章和修改bug
  • 您也可以将例如日常bug. 未来feature等的功能性PR, 申请提交到我们的的主仓库。

参与共建

关于如何提PR请先阅读以下文档

贡献指南

此项目遵循贡献者行为准则。参与此项目即表示您同意遵守其条款.

FlutterGo 期待你我共建~

具体 pr 细节和流程可参看 FlutterGo README 或 直接钉钉扫码入群

学习交流

关注公众号: 【全栈前端精选】 每日获取好文推荐。还可以入群,一起学习交流呀~~

大揭秘!“恐怖”的阿里一面,我究竟想问什么

原文链接地址:Nealyang/personalBlog

市面上有很多关于面试的文章,但是基本都是从应聘者的角度去分析问题的,从招聘官的角度去分享的着实不多。本文将从我的个人招聘经历分享下关于前端一面的一些思考和自己的感悟。以下所有感悟皆为笔者个人感悟,不代表任何。有不妥之处,欢迎指出

其实不得不说,找工作,真的七分实力,三分运气。不同的面试官有不同的看重点,所以千万不要为一次的滑铁卢而丢失信心。

面试环节

关于面试题的答案讲解,本文将不做非常详细的分析。具体的每个知识点,笔者后面尽量两周更新一篇相关知识点文章于公众号 全栈前端精选 中,欢迎关注、讨论和分享。下图是笔者计划后续写的总结性技术文章。

自我介绍

基本面试这是必然的开场,笔者在公司也稍微面试过不少人吧,高峰期基本每晚都要面试一两个,听过了各种各样的开场介绍。这里简单说下笔者作为面试官比较喜欢和不喜欢的介绍吧。

我叫 xxx,毕业于(目前就职于) xxx,来自 xxx,技术栈 xxxx,喜欢 xxx。。。

类如上述的自我介绍,其实很多都在简历上写明了,甚至很多是我并不关心的。

我只关心你能力和我职位的匹配度。所以诸如此类的简介,笔者更是希望能够简短。笔者作为面试的时候,更喜欢听到的是我做过什么牛 x 的项目,这个项目有多难,如何攻克的,以及这个项目做完你收获到了什么,甚至这个项目做完,该项目对团队、部门甚至公司而言,带来哪方面的提高。或者可以介绍你在校获得了什么奖项,意味着什么、成长了什么。

如上的介绍,其实就能够让面试官眼前一亮,因为能看到你对这个项目的思考、以及这个项目对你的历练。

划重点:我们都知道下一个面试环节是知识点提问,所以这里的难,可以适当的抛出技术的难点在哪。引起面试官的兴趣,从而去提问面试的节奏我们要学会自己掌握,别老是被面试官牵着走

走到这一步,基本有如下两个分支:

  • 面试官对你的这个项目(奖项)比较感兴趣,会接着问下去,然后问其中技术的实现细节。(所以这里自己千万不要吹牛x,然后补不回来)
  • 面试官不是很感兴趣(很可能是面试官的技术盲区,比如我就这样。哈哈),然后问自己准备的一些面试题。。。

这里需要说明的,在笔者面试应聘 p7 的同学的时候,会更喜欢到你对这个项目的思考,诸如会问一下题目:

  • 现有的技术方案、行业对比
  • 你觉得你做过的项目或发起过的优化里面最有价值是的哪个?为什么?对业务的帮助是什么?
  • 你做的东西可以复用于其他团队吗?
  • 横向与市场已知的 xxx 解决方案,你们的优势在哪?

上述的这些思考,其实目前我也达不到。但是,这不一定要求面试官一定要达到这个水准,项目的思考维度也还没有这么的深入。没吃过猪肉还没见过猪跑嘛。

面试题

如上面所说的,如果面试中应聘者说到了笔者比较感兴趣的技术方向、或者技术点,那么笔者就会直接问下去。如果说到了笔者不是很擅长的技术区域,那么我笔者就不会追问技术细节了。

整体一面的时间大概也就半小时左右,加上前后的介绍,基本题目就四五题吧。笔者面试没有固定的题目,通常根据应聘者的经历而问。这里举例下在上面的介绍毫无亮点可言的时候(基本凉了一半),笔者喜欢问的一类题目吧。

基础题目考核

JavaScript 面向对象的理解和感悟

基本刚开始问题的题目都是比较简单和考核基础的,比如有的时候笔者第一题一般问:** JavaScript 面向对象的理解和感悟**、题目非常的开放。给了你足够大的舞台表现自己。

说下这题在笔者面试别人时候的心里打分点:

  • 首先,我肯定是需要你告诉我,什么是面向对象,面向对象有哪些特点,以及这些特点的解释。
  • JavaScript 如何实现这些特点,比如封装、继承、多态。如果关于上述三点,你能够解释到有多少种实现方式、优缺点是什么。以及近几年流行的解决方案是什么。这就是加分 ,比如对于继承吧。类式继承、构造函数继承、组合继承、原型继承、寄生组合继承等等,说出大概的实现思路和优缺点,再介绍下 extends 或者 mixin 的实现甚至你可以衍生到JavaScript 的模块化发展甚至到为什么现在 TS 如此流行。那么可以说到这一环节解答的就非常棒了。
  • 回答完 JavaScript 的面向对象,是不是可以从此衍生下为什么需要面向对象。以及当先对于软件设计的高内聚、低耦合的思考?来个对此题一个提纲挈领的总结?

综上所述,其实不难看出,越是这种基础且开放的题目,可以是一个陷阱,更可以是一个机会。因为一道题真的可以全方面的感受到应聘的基础是否扎实。

后面的题目的笔者基本喜欢根据应聘者的上一题的回答中甚至应聘者随口说到的知识点,继续追问。但是限于此文为分享文章,这种形式很难演示。下面就继续介绍下后续的题目。

浏览器输入 url 到页面的展现,具体发生了些什么可以展开说下么

断于上述题目知识点。第二个问题笔者通常喜欢问一些考察可深可浅的一些题目,注入:浏览器输入 url 到页面的展现,具体发生了些什么可以展开说下么

基本回答都是

  • 在浏览器地址栏输入URL
  • 浏览器解析URL获取协议,主机,端口,path
  • 浏览器组装一个HTTP(GET)请求报文
  • 浏览器获取主机ip地址
  • 打开一个socket与目标IP地址,端口建立TCP链
  • TCP链接建立后发送HTTP请求
  • 服务器将响应报文通过TCP连接发送回浏览器,浏览器接收HTTP响应
  • 根据资源类型决定如何处理(假设资源为HTML文档)
  • 解析HTML文档,构件DOM树,下载资源,构造CSSOM树,执行js脚本
  • 最后展现出来给用户

基本如果应聘者只回到了上述步骤,很多关键步骤(前端应该了解的知识点)没有提及,那么基本凉凉一半了。这里简述下笔者感觉,这其中你应该具体展开说明的。

  • 浏览器发送请求,是否需要查看缓存?是否请求资源在缓存中并且新鲜,跳转到转码步骤?如果资源已经缓存,是否新鲜?如何检查?怎么判断、http1.0 和 http1.1 的区别是什么,这些字段的优先级是怎么样子的。
  • 浏览器解析 url 获取协议,过程是什么?DNS 递归查询可否介绍下?
  • 建立 TCP 链接的三次握手是否可以介绍下
  • 服务器接受到请求,是否需要检查缓存?检查什么字段?什么样的缓存会需要服务端检查?
  • 服务端发送 TCP 链接,浏览器接受 http 相应后,根据什么来决定是否需要关闭连接?关闭 TCP 的四次挥手是什么?
  • 浏览器是否需要检查状态码,有哪些状态码?(笔者高频考码:304、200)
  • 在解析的时候,具体如何解析、是否有顺序。(重绘重排高频考题就在这里)
  • 总结如上、我们是否可以给出一些基本的网站优化手段???

上述题目的每一步展开,都将会是下一个面试题。

具体的知识点介绍,不是此文主要讲解内容,这里就不多言了。

解决问题能力考查

其实上面两(大)题后,基本基础、网络、浏览器、js 执行、优化都已经考核到。对于 p6 的一个 job model 还有一项是对于问题的解决能力。

其实这里一般都是出一道问题,然后你给出一些你的实现思路,这里就不做距离了,因为太开放!

比如:

  • 诸如我现在需要监听那种频繁发生的事件,你有那些优化么
  • 埋点的实现思路
  • 非递归的二叉树遍历
  • 文件上传断点、续传
  • 设计模式的应用场景考核

等等

切记:这类题目,一定不要说不会、不知道。哪怕真的不知道,也要给出大概的解答思路和实现思路。哪怕不对!一定要讲出自己的思考过程

进阶题目考核

在面试 p6、p7 的时候,一般后面还会跟一道进阶题目(根据应聘者具体情况而定)。

没有开放性的答案其实,所以这里笔者就不细述有哪些思考和想听到的点了。只要你说的对就行其实

  • VUE 双向绑定原理
  • VUE/React diff 算法的大概思路
  • 现有的状态管理的实现
  • webpack中 loader、plugin 的实现思路
  • 简易版 webpack 的实现
  • KOA、Express 中间件的实现
  • React Filter 的理解和原理
  • 前端构建工具的、vue-cli、create-react-app 的原理和实现思路
  • 等等。。。。。

结束环节

半小时,基本只能问四五题这样,说实话,题目的考核大概能占参考度的 90%,还有 10%可能就是言语和感觉了。那么对于结束的时候,说下不好的感觉:

最后,你有什么要问我的嘛

我想咨询下,我能不能通过这次面试,我对工资无所谓,我愿意学习,特别希望能够进入 xxx 跟大牛一起学习,历练。

讲真,这类的话听过很多次了。如果在看的你也命中了这个。我想说,其实公司招我们进来,是搬砖的,不是给我们学习的。学习是你自己的需要,不是为了公司学的。说这类的话,太给自己降价了!

说说笔者应聘的时候,一般结束的你有什么要问我的嘛的回答

  • 我比较在意自己的技术方向和职业发展,能够简单介绍下如果我面试上贵公司职位,我以后的工作内容和在团队的价值么?
  • 想了解下公司对于前端的重视程度以及在大前端时代,团队对于技术的思考

大概就是笔者会问,我这个职位是干嘛的?在公司有么有价值?跟我自己的职业、技术规划是否吻合?

这样!面试官会觉得,恩~这小子有思考~~~再者,如果面试官给你介绍的非常非常详细,那么其实从侧面就说明,他对你很满意了!已经到了面试官开始极力展现自己的时候了~~

就比如:医生,我老婆怀的是男孩还是女孩啊? 不能问!!!这是政策

但是:医生,我这孩子出生我给他起名字叫王刚蛋你看合适不? _恩。。。不太合适_ 基本可能怀的是女孩了

最后

其实我想说,面试,不仅仅看运气和实力,其实有的时候也是一场心理的博弈。

你的每一次回答都可能引出下一道面试题。有意识无意识的留点回答漏斗也是一种带节奏的方式。

如果面试官提出一个问题,你都抓不住他的考点,那基本要凉~~

其次,我想说,上面我说的开放性基础题的回答,大家千万不要误解为回答的多就是好。千万不要一个题目回答了半个小时,没必要!点到为止,证明自己考虑到、有这个知识储备即可,不要回答的让面试官都烦了。

然后,对于如何拿到面试的敲门砖:简历。没有那么多可说,也不是没得说。

简而言之:面试官筛选简历是非常枯燥的一件事情。基本是一眼带过,不会在简历上撇超过 40s。

所以:

  • 简历要整洁,简洁、简洁。真的别密密麻麻都是字,没那么多耐心看的。
  • 重点突出,可以加粗或者颜色标识。比如:自己开源类 React 框架
  • 简历是一份介绍更是一个成绩单,既然是成绩单,成绩一定要吐出:开源项目 15k star推动公司技术建设 等等
  • 不得不说,名校和大厂的背景。很吸睛。

最后,秋招开始了,祝福所有找工作的同学,都能顺顺利利拿到 offer!加油~

你懂得

对,文章最后往往都是广告环节~~嘎嘎嘎

阿里秋招

阿里秋招正式开始啦!!!

Bu 介绍请看:号外!号外!阿里拍卖提前秋招!!!

简而言之! 2020 届毕业生,跪求简历呀 我把面试题都告诉你了呢,还不发个简历过来:[email protected]

学习交流

关注公众号: 【全栈前端精选】 每日获取好文推荐。还可以入群,一起学习交流呀~~

一个优秀的前端都应该阅读这些文章

前言

的确,有些标题党了。起因是微信群里,有哥们问我,你是怎么学习前端的呢?能不能共享一下学习方法。一句话也挺触动我的,我真的不算是什么大佬,对于学习前端知识,我也不能说是掌握了什么捷径。当然,我个人的学习方法这篇文章已经在写了,预计这周末会在我个人公众号发布。而在此之前,我想展(gong)示(xiang)一下,我平时浏览各个技术网站,所记录下来的文章。如果你能做到每日消化一篇,或许,你只要一年,就能拿下各个大厂 offer!

不由感慨,好文太多!吾等岂能浪费,还整日怨天尤人。

个人好文收藏

收藏截止时间:2019-07-24 11:50:49

typescript

CSS

前端工程(架构、软实力)

React 技术栈

webpack/babel

Test

JavaScript

Node

Flutter

Http

浏览器

面试

数据结构与算法

其他

结束语

以上包括我已读还未移至已读的记录中(主要是由于感觉还需再度)。所有文章,我都会好好学习,没办法,毕竟比较菜。还有太多需要学习。

欢迎关注我个人微信公众号:全栈前端精选

我会每日推荐各种精选好文,以及每日一道面试题讲解。(今日才开启这个计划)

沸点 UI & 功能 编写(上)

介绍

这一章节代码量可能会比较大,我们将完成沸点的UI以及相应功能编写,完成此篇,你将得到如下的界面效果:

数据准备

由于之前的首页编写中已经介绍了关于本地数据model的使用,这里我们将直接使用线上数据来进行我们的代码编写

通过掘金web版的公开api我们可以知道沸点的请求api地址

  • lib/api.dart
  // 沸点
  static const String PINS_LIST = 'https://short-msg-ms.juejin.im/v1/pinList/recommend';

从沸点的每一个cell中,我们需要去分析构成该UI,大致需要的字段,通常,在我们的项目开发中,这些也是与开发约束的。

通过分析,我们可以看到沸点的每一个cell分为两种,文字+图片 以及文字+链接的形式,当然,其中每一个沸点也可能没有图片,也有的沸点包含主题、文字中含有链接。这些,在我们定义 沸点的数据model的时候都应该包含进去,所以如下,我们提取我们需要字段

  • lib/model/pins_cell.dart
  Map<String, dynamic> user;
  String objectId;
  String uid;
  String content;
  List<String> pictures;
  int commentCount;
  int likedCount;
  String createdAt;
  Map<String, dynamic> topic;
  String url;
  String urlTitle;
  String urlPic;

在数据model中,应该包含他的构造函数以及factory中对请求数据的处理

  factory PinsCell.fromJson(Map<String, dynamic> json) {
      Map<String, dynamic> user = new Map();
      user['avatarLarge'] = json['user']['avatarLarge'];
      user['objectId'] = json['user']['objectId'];
      user['company'] = json['user']['company'];
      user['jobTitle'] = json['user']['jobTitle'];
      user['role'] = json['user']['role'];
      user['userName'] = json['user']['username'];
      user['currentUserFollowed'] = json['user']['currentUserFollowed'];
  
      Map<String, dynamic> topic = new Map();
      // 有的沸点没有topic
      if (json['topic'] != null) {
        topic['objectId'] = json['topic']['objectId'];
        topic['title'] = json['topic']['title'];
      }
  
      List<String> pics = new List();
      // pics = json['pictures'];_TypeError (type 'List<dynamic>' is not a subtype of type 'List<String>')
      json['pictures'].forEach((ele) {
        pics.add(ele);
      });
  
      return PinsCell(
          commentCount: json['commentCount'],
          content: json['content'],
          createdAt: Util.getTimeDuration(json['createdAt']),
          likedCount: json['likedCount'],
          objectId: json['objectId'],
          pictures: pics,
          topic: topic,
          uid: json['uid'],
          url: json['url'],
          urlPic: json['urlPic'],
          urlTitle: json['urlTitle'],
          user: user);
    }
  • 注意上面代码中关于Map和list数据类型的处理,这里我们是不能够直接复制的,否则会出现_TypeError (type 'List<dynamic>' is not a subtype of type 'List<String>')的错误,也就是数据类型转换的问题。 所以对于Map以及List的数据类型,这里我们单独拿出来通过遍历来重新赋值的。
  • 关于沸点的topic字段,有些沸点是不存在的,所以这里我们需要加一层判断,否则在直接取值的时候会报错。当然,这个注意项可能更加的设计到业务一些

定义好数据model后,我们去编写请求方法

  • lib/util/data_util.dart
  // 沸点 列表

  static Future<List<PinsCell>> getPinsListData(
      Map<String, dynamic> params) async {
    List<PinsCell> resultList = new List();
    var response = await NetUtils.get(Api.PINS_LIST, params: params);
    var responseList = response['d']['list'];
    for (int i = 0; i < responseList.length; i++) {
      PinsCell pinsCell;
      try {
        pinsCell = PinsCell.fromJson(responseList[i]);
      } catch (e) {
        print("error $e at $i");
        continue;
      }
      resultList.add(pinsCell);
    }

    return resultList;
  }
  • 数据请求同样适用我们在net_util.dart下封装的get和post方法
  • 拿到数据后根据数据结构获取列表数据封装成我们的Pins model

编写沸点页面UI

沸点页面,我们需要一些变量阿里存储页面信息,比如沸点list、请求参数、翻页等

  • lib/pages/pins_page.dart
  List<PinsCell> _listData = new List();

  Map<String, dynamic> _params = {
    "src": 'web',
    "uid": "",
    "limit": 20,
    "device_id": "",
    "token": ""
  };
  bool _isRequesting = false; //是否正在请求数据的flag
  bool _hasMore = true;
  String before = '';
  ScrollController _scrollController = new ScrollController();

编写相关的请求方法,然后在页面初始化的时候调用,

    void getPinsList(bool isLoadMore) {
      if (_isRequesting || !_hasMore) return;
  
      if (before != '') {
        _params['before'] = before;
      }
      if (!isLoadMore) {
        _params['before'] = '';
      }
      _isRequesting = true;
      before = DateTime.now().toString().replaceFirst(RegExp(r' '), 'T') + 'Z';
      DataUtils.getPinsListData(_params).then((resultData) {
        List<PinsCell> resultList = new List();
        if (isLoadMore) {
          resultList.addAll(_listData);
        }
        resultList.addAll(resultData);
        if (this.mounted) {
          setState(() {
            _listData = resultList;
            _hasMore = resultData.length != 0;
            _isRequesting = false;
          });
        }
      });
    }
  • 当页面正在请求、以及当前页已经是最后一页的时候,不进行请求
  • 里面的before字段是掘金web版请求网络数据翻页的字段,这里跟业务相关,我们可以不去关心
  • 使用我们之前在dataUtil中封装的请求方法,在获取请求数据后,如果是loadMore,则需要将之前list数据叠加,否则为直接赋值。同时需要设置页面的 isRequesting hasMore字段
  • 注意这里我们setState之前判断了页面的mounted,因为在页面退出时我们需要销毁Controller,而请求是异步操作,所以如果我们在页面已经销毁的时候进行setState操作,页面会报错。

    @override
    Widget build(BuildContext context) {
      if (_listData.length > 0) {
        return Container(
          color: Color(0xFFF4F5F5),
          child: ListView.builder(
            itemCount: _listData.length + 1,
            itemBuilder: _itemBuilder,
            controller: _scrollController,
          ),
        );
      } else {
        return Center(
          child: CircularProgressIndicator(),
        );
      }
    }

build方法中比较简单,其实就是初始化一个列表。这里再强调下,使用ListView.builder去实现长列表是非常好的选择,其性能也是非常的优越,会进行一些数据回收工作。

在initState的时候,我们进行一些页面的请求和Controller的初始化工作

  @override
  void initState() {
    getPinsList(false);
    super.initState();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        print('loadMore');
        getPinsList(true);
      }
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

页面完整UI地址为:pins_page.dart

编写沸点cell

沸点的cell其中有一个难点是content中加载这url,然后url需要翻译成图片,如下:

img 而从我们获取到的字段来看,这就是单纯的url,所以这里我们需要正则提取相关字段,然后进行拼接。

    List<Widget> _buildContent(String content) {
      List<Widget> contentList = new List();
      RegExp url = new RegExp(r"((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+");
      List listString = content.split(url);
      List listUrl = new List();
      Iterable<Match> matches = url.allMatches(content);
      int urlIndex = 0;
      for (Match m in matches) {
        listUrl.add(m.group(0));
      }
      for (var i = 0; i < listString.length; i++) {
        if (listString[i] == '') {
          // 空字符串说明应该填充Url
          contentList.add(PinsCellLink(
            linkUrl: listUrl[urlIndex],
          ));
          urlIndex += 1;
        } else {
          contentList.add(Text(
            listString[i],
            style: _textStyle,
            overflow: TextOverflow.ellipsis,
            maxLines: 5,
          ));
        }
      }
      return contentList;
    }
  • 首先我们new一个匹配url的正则RegExp(r"((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+")
  • 将content的数据按照url去分割成数组。将url正则匹配出来的url存到数组中。
  • 最后通过遍历来填充之前挖去的字段

这里面我们将文字中的链接抽出来作为一个widget

  • lib/widgets/pins_cell_link.dart
class PinsCellLink extends StatelessWidget {
  final String linkUrl;

  PinsCellLink({Key key, this.linkUrl}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final Color textColor = Theme.of(context).primaryColor;
    return Container(
      width: 100.0,
      child: InkWell(
        onTap: () {
           Application.router.navigateTo(context, "/web?url=${Uri.encodeComponent(linkUrl)}&title=${Uri.encodeComponent('掘金沸点')}");
        },
        child: Row(
          children: <Widget>[
            Icon(
              Icons.link,
              color: textColor,
            ),
            Text(
              '网页链接',
              style: TextStyle(color: textColor),
            )
          ],
        ),
      ),
    );
  }
}

代码地址为:pins_cell_link.dartpins_list_cell.dart

在cell的content中,这些widget还是需要平铺并且换行展示的。所以这里我们使用 Wrap widget

  Widget _renderContent(String content) {
    return Wrap(
      direction: Axis.horizontal,
      verticalDirection: VerticalDirection.down,
      spacing: 10.0,
      children: _buildContent(content),
    );
  }
  • Wrap widget允许我们设置子widget的排列方式,这里我们设置方向为Axis.horizontal横向排列,然后允许我们组件换行。这样就会出现得到我们想要的效果

img

总结

如上,我们只是完成了沸点列表页面的一部分,限于篇幅和知识点的吸收,我们将沸点cell的剩余代码编写放到下一章节中。下一章节,我们将完成图片的查看、轮播图,设置页面切换动画以及图片和链接的cellUI编写。

沸点 UI & 功能 编写(下)

前言

上一章节中,我们完成了沸点页面部分UI的编写,这一章节,我们将完成沸点页面剩余页面的代码编写

cell组件编写

通过拿到的cell数据去判断为哪一类cell

  widget.pinsCell.url == ''
                    ? PinsCellPic(
                        pics: widget.pinsCell.pictures,
                      )
                    : PinsCellUrl(
                        url: widget.pinsCell.url,
                        urlPic: widget.pinsCell.urlPic,
                        urlTitle: widget.pinsCell.urlTitle,
                      ),
  • 当cell中的数据包含url这个的时候,说明是link类型的cell

cell类型的UI编写比较简单,我们将完成如下的UI编写
img

  • lib/widget/pins_cell_url.dart
  @override
    Widget build(BuildContext context) {
      return InkWell(
        onTap: (){
          Application.router.navigateTo(context,"/web?url=${Uri.encodeComponent(url)}&title=${Uri.encodeComponent(urlTitle)}");
        },
            child: Container(
            padding: const EdgeInsets.all(10.0),
            margin: const EdgeInsets.symmetric(horizontal: 14.0,vertical: 10.0),
            height: 100,
            decoration: BoxDecoration(
                border:
                    Border.all(color: Theme.of(context).accentColor, width: 1.0),
                borderRadius: BorderRadius.all(Radius.circular(4.0))),
            child: Row(
              children: <Widget>[
                Expanded(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
  
                    children: <Widget>[
                      Text(
                        urlTitle,
                        style:
                            TextStyle(fontSize: 19.0, fontWeight: FontWeight.bold),
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                      Text(
                        url,
                        style: TextStyle(
                            fontSize: 15.0, color: Theme.of(context).accentColor),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ],
                  ),
                  flex: 2,
                ),
                Expanded(
                  flex: 1,
                  child: Image.network(urlPic),
                )
              ],
            )),
      );
    }
  • 最外层使用InkWell包裹,这个应该不默认,其实就是为了绑定点击事件,点击后,跳转到链接地址
  • 外层通过Container 包裹,去设置一些样式,比如padding、margin、height以及border的设置
  • 里面我们通过Row去横向排列子组件,里面我们使用Expanded组件,是为了能够完全的撑开Row的内容

图片查看页面

下面我们新建一个页面,用于查看图片

  • lib/pages/swip_page.dart

    @override
    Widget build(BuildContext context) {
      List picList = pics.split(',');
      int index = int.parse(currentIndex);
      return Center(
          child: Swiper(
            itemBuilder: (BuildContext context, int index) {
              return new Image.network(
                picList[index],
                fit: BoxFit.fitWidth,
                width: MediaQuery.of(context).size.width,
              );
            },
            itemCount: picList.length,
            scale: 0.8,
            pagination: new SwiperPagination(),
            index: index,
            onTap: (index) {
              Application.router.pop(context);
            },
          ),
        );
    }
  • 这里我们引入flutter package:flutter swiper。关于如何引入以及查找相关package,之前已经介绍过,这里就不多说了。
  • 设置itemCount为我们传入的图片数目。并且设置指示器
  • 点击页面的时候,退出页面

lib/routers 下注入相关页面后,即可来编写cell的图片部分,并完成页面的跳转以查看图片

  • lib/widget/pins_cell_pic.dart
  @override
    Widget build(BuildContext context) {
      if (pics.length > 3) {
        _picHeight = 190.0;
      }
      for (int i = 0; i < pics.length; i += 3) {
        List<Widget> _tempRow = List();
        _tempRow.add(
          Expanded(
            child: InkWell(
              onTap: () {
                Application.router.navigateTo(context,
                    '/swip?pics=${Uri.encodeComponent(_buildPicsStr())}&currentIndex=${i.toString()}',transition: TransitionType.fadeIn);
              },
              child: Image.network(
                pics[i],
                fit: BoxFit.cover,
                height: _picHeight,
              ),
            ),
            flex: 1,
          ),
        );
        if (i + 1 < pics.length) {
          _tempRow.add(
            SizedBox(
              width: 10.0,
            ),
          );
          _tempRow.add(
            Expanded(
              child: InkWell(
                onTap: () {
                  Application.router.navigateTo(context,
                      '/swip?pics=${Uri.encodeComponent(_buildPicsStr())}&currentIndex=${i.toString()}');
                },
                child: Image.network(
                  pics[i + 1],
                  fit: BoxFit.cover,
                  height: _picHeight,
                ),
              ),
              flex: 1,
            ),
          );
        }
        if (i + 2 < pics.length) {
          _tempRow.add(
            SizedBox(
              width: 10.0,
            ),
          );
          _tempRow.add(
            Expanded(
              child: InkWell(
                onTap: () {
                  Application.router.navigateTo(context,
                     '/swip?pics=${Uri.encodeComponent(_buildPicsStr())}&currentIndex=${i.toString()}');
                },
                child: Image.network(
                  pics[i + 2],
                  fit: BoxFit.cover,
                  height: _picHeight,
                ),
              ),
              flex: 1,
            ),
          );
        }
        _wrapChildren.add(Container(
          child: Row(
            children: _tempRow,
          ),
          margin: const EdgeInsets.only(bottom: 10.0),
        ));
      }
  
      return Container(
        margin: const EdgeInsets.symmetric(vertical: 10.0),
        child: Wrap(
          children: _wrapChildren,
        ),
      );
    }
  • 首先我们需要注意的是,这里我们不能使用GridView来实现我们的图片,除非我们能够给最外层一个限定高度的Container,但是,根据我们页面的UI,这个高度是不能固定的。且,当我们给定了Container一个固定高度,Container内容还是会有滚动标签在。所以这里我们不适用GridView布局。
  • 点击跳转页面,并且传递当前点击的图片的index
  • 根据图片的数目不同,设置不同的图片高度。
  • 跳转页面的时候,使用了TransitionType.fadeIn来设置页面的跳转动画,这些大家都可以自行设置查看相应效果。注意TransitionType属于fluro的方法,所以使用时需要引入包:import 'package:fluro/fluro.dart';

完成代码查看:代码地址

效果如如下:

swip

代码中同级目录包括沸点cell的底部 点赞和评论的widget的编写,地址为:lib/widget/pins_cell_bottom_button.dart 代码比较简单也比较常规,大家可以自行查看编写相应UI。

cell widget 组合

完成了沸点cell的每一个小部分的代码编写,下面将这些小组件拼接成我们完整的一个沸点的cell吧

  • lib/widgets/pins_list_cell.dart
    @override
    Widget build(BuildContext context) {
      return Container(
        color: Colors.white,
        margin: const EdgeInsets.only(top: 10.0),
        child: Column(
          children: <Widget>[
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 9.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.max,
                children: <Widget>[
                  PinsCellHeader(
                      userInfo: widget.pinsCell.user,
                      createdAt: widget.pinsCell.createdAt),
                  _renderContent(widget.pinsCell.content),
                  widget.pinsCell.url == ''
                      ? PinsCellPic(
                          pics: widget.pinsCell.pictures,
                        )
                      : PinsCellUrl(
                          url: widget.pinsCell.url,
                          urlPic: widget.pinsCell.urlPic,
                          urlTitle: widget.pinsCell.urlTitle,
                        ),
                  widget.pinsCell.topic.length == 0
                      ? Container()
                      : PinsCellTopic(
                          topicInfo: widget.pinsCell.topic,
                        ),
                ],
              ),
            ),
            PinsCellBottomButton(
              commentCount: widget.pinsCell.commentCount,
              likedCount: widget.pinsCell.likedCount,
            ),
          ],
        ),
      );
    }

这部分代码就是将我们编写的所有颗粒度非常小的widget组合使用成我们需要的UI。

总结

如上,我们就完成了沸点部分的编写。需要注意的是,这部分UI非常的多而杂。但是其实难度都不是很大,将大的页面,细分为一个一个颗粒度非常细的组件使我们应该学会的。注意,一定要根据自己的理解、业务的需求去合理划分组件。不可盲目细化导致杂乱。

项目地址

Nealyang 全栈前端

这不是干货铺,更不是学习平台。这仅仅是个人的学习、感悟和成长的总结笔记。只是,爱分享、爱沉淀、爱总结。

关于 Nealyang

个人网站:https://www.nealyangfed.com (暂未开通)

GitHub:https://github.com/Nealyang

当然,在前端大潮中我始终也还是个菜鸟,还在不断地探索学习。慢慢的在迷茫中,寻求自己的术方向,在堆积满满的业务需求中,祈求再次寻求技术的突破。

工作中,主要使用技术栈包括:React 技术栈、Rax、weex、Kissy、Flutter、Koa、Midway、Ts 等等

涉及到的领域包括:pc 页面、手机客户端(目前主要是手淘)、Flutter(FlutterGo 主开发者)、研究性项目后台开发等

YY 未来

预期说对于未来的思考,不如说说对于技术的追求吧。

还是希望自己能够全面掌握,吃透一门。

可能对于现在的我,更希望对于前端目前所有的知识体系有个点到面到深度的一个掌握和认识吧。在这个过程中,找到自己喜欢的点,去挖个深度。

所以这个过程中,会有很多的个人学习总结、感悟感想等,也就是,此公众号诞生的初衷了。

当然,所有的技术回归到初衷,都需要落实于业务。不然都是扯淡。

Nealyang 全栈前端

扫码关注微信公众号

首先,我不想技术仅仅局限于前端,但是不离前端。也就是说,我肯定会去分享关于客户端相关知识(weex、RN、Flutter)或者后端相关(nodejs、java、数据库等),所以公众号命名为 全栈前端。

再回到自己初衷,这个“全栈”,并非围绕“前端”展开,所以前面加上了作用范围 “Nealyang”.

如果你也在迷茫,如果你也希望学习、提高、进步。欢迎关注微信公众号: Nealyang 全栈前端 。

学习交流

一起学习,一起进步

QQ 群

React技术栈:398240621

nodejs技术1群:209530601(已满)

nodejs技术2群:698239345

前端技术杂谈:604953717

微信群

加入任一 qq 群艾特我~

或者直接在微信公众号里回复即可

首页List UI编写

介绍

这一章节中,我们将完成首页List部分的编写

该章节后,咱们的list界面应该长这个样子
img

cell 结构分析

cell

cell的整体是一个Column结构,每一行中,再是一个横向排列的。底部的点赞和评论按钮结构比较统一,可以考虑单独抽出来作为一个新的widget

作者和标题之间的点(.)无法通过文字直接模拟,所以这里我们把点也同样抽出来作为一个widget

cell 相关widget编写

个人编写习惯:先从小写到大,在大(iindexListCell)中引入你要写的小(inTextDot)以方便看样式,然后编写玩小的后再放到大的中使用.

  • lib/widgets/inTextDot.dart
  import 'package:flutter/material.dart';
  
  class InTextDot extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Container(
        width: 3.0,
        height: 3.0,
        margin: const EdgeInsets.symmetric(horizontal: 6.0),
        decoration: BoxDecoration(
            color: Color(0xFFB2BAC2),
            borderRadius: BorderRadius.all(Radius.circular(3.0))),
      );
    }
  }

文字中的点,比较容易,我们直接写一个container,用于做点,然后边距也作为这个widget该有的属性。注意看这里我们样式中对于Container自身的装饰属性所谓decoration,同样我们之前说的vscode功能,鼠标悬停在Widget上,可以看到他的属性,以及这个属性的值类型

de

  • lib/widgets/goodAndCommentCell.dart
    code
    这里点赞和评论的cell很简单,其实仔细分析可以看到,点赞和评论也是同一个组件才对,不应该写两遍。这里我就不优化,之所以是截图,希望大家可以自己动手,发挥自己的聪明才智,编写这一个UI,或者说,优化我上面的代码~

goodAndCommentCell 代码链接

  • lib/widgets/indexListCell.dart
    首先我们定义一些基础变量
  final IndexCell cellInfo;

  IndexListCell({Key key, this.cellInfo}) : super(key: key);

  TextStyle titleTextStyle = TextStyle(
    color: Color(0xFFB2BAC2),
    fontWeight: FontWeight.w300,
    fontSize: 13.0,
  );

由于第一行中,热门、专栏、推荐等是不一定存在的,所以这里我们需要写一个方法去判断是否存在这个字段

  List<Widget> _buildFirstRow() {
      List<Widget> _listRow = new List();
      if (cellInfo.hot) {
        _listRow.add(Text(
          '热',
          style: TextStyle(
            color: Color(0xFFF53040),
            fontWeight: FontWeight.w600,
          ),
        ));
        _listRow.add(InTextDot());
      }
      if (cellInfo.isCollection == 'post') {
        _listRow.add(Text(
          '专栏',
          style: TextStyle(
            color: Color(0xFFBC30DA),
            fontWeight: FontWeight.w600,
          ),
        ));
        _listRow.add(InTextDot());
      }
      _listRow.add(Text(cellInfo.username, style: titleTextStyle));
      _listRow.add(InTextDot());
      _listRow.add(Text(cellInfo.createdTime, style: titleTextStyle));
      _listRow.add(InTextDot());
      _listRow.add(Expanded(
        //防止文本超长
        child: Text(
          cellInfo.tag,
          style: titleTextStyle,
          overflow: TextOverflow.ellipsis,
        ),
      ));
      return _listRow;
    }

而cell的布局如上我们分析的那样,Column中一行一行的搞下去

所以此处build方法如下:

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(10.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Container(
            height: 20.0,
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: _buildFirstRow(),
            ),
          ),
          Container(
            margin: const EdgeInsets.symmetric(vertical: 9.0),
            child: Text(
              cellInfo.title,
              style: TextStyle(
                color: Color(0xFF393C3F),
                fontSize: 14.0,
                fontWeight: FontWeight.w600,
              ),
              overflow: TextOverflow.ellipsis,
            ),
          ),
          GoodAndCommentCell(cellInfo.collectionCount, cellInfo.commentCount),
          SizedBox(
            height: 15.0,
          ),
          Divider(
            height: 2.0,
          ),
        ],
      ),
    );
  }

对于一些样式的调整和边距的调整,大家可以根据自己的审美哈~实际开发中,这些也根据设计师给我们的样式来做相应开发。

头部header

分析线上页面,我们可以看到在列表的cell上部,还有个头部,在我们登陆状态和非登录状态是不同的,所以这里,我们肯定是需要给他一个Widget组件来实现的

  • lib/widgets/indexListHeader.dart
  import 'package:flutter/material.dart';
  
  class IndexListHeader extends StatelessWidget {
    final bool hasLogin;
  
    IndexListHeader(this.hasLogin);
  
    @override
    Widget build(BuildContext context) {
      if (hasLogin) {
      } else {}
      return Container(
        padding: const EdgeInsets.all(10.0),
        child: Column(
          children: <Widget>[
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                Text(
                  '热门文章',
                  style: TextStyle(
                    color: Color(0xFF434343),
                    fontSize: 16.0,
                    fontWeight: FontWeight.bold
                  ),
                ),
                FlatButton(
                  child: Text('查看更多', style: TextStyle(color: Color(0xFF757575))),
                  onPressed: () {
                    print('查看更多');
                  },
                )
              ],
            ),
            Divider(),
          ],
        ),
      );
    }
  }

这里我们还未涉及到登陆,所以暂时先留一个口子(构造函数)在这,后面做到了登陆部分再回来修改UI。

之前我们说到在使用一些widget的时候,有些属性是必须的,不然编译器会报错,like this

worning

当然,我们可以按住command键,点击widget,到源码中去查看一些相关属性的编写和类型

ListView.builder

毫无疑问,这里我们使用ListView 布局,因为考虑性能且不确定长度,这里我们不会给ListView传入children而是直接只用它的构造函数 builder,而且这样的长列表对于非可视区域部分的cell还会做相应的资源回收。

  • lib/pages/indexPage.dart
  _renderList(context , index){
    if(index == 0){
      return IndexListHeader(false);
    }
    return  IndexListCell(cellInfo: _listData[index-1]);
  }
  
  @override
  Widget build(BuildContext context) {
      print(_listData.length);
      if (_listData.length == 0) {
        return Center(
          child: CircularProgressIndicator(),
        );
      }
      return ListView.builder(
        itemCount: _listData.length+1,//添加一个header
        itemBuilder: (context,index)=> _renderList(context,index),
      );
    }
  }

注意上面我们给ListView传递的长度是length+1,因为我们要塞列表的头部~

同样,我们也处理了数据未请求到的时候loading的展示

至此,我们的app已经有点样子出来了有么有

总结

如上,我们基本首页已经有了雏形,至此,你应该学会

  • ListView的使用
  • 组件的拆分、细化和组合
  • VsCode的一些使用技巧

项目框架搭建

准备工作

笔者 Flutter 版本及环境信息:Channel beta, v0.11.9, on Mac OS X 10.14.1 18B75

版本更新向前兼容,笔者更新版本后,项目依旧运行如初。目前笔者的flutter版本信息为:

    Flutter 1.0.0 • channel beta • https://github.com/flutter/flutter.git
    Framework • revision 5391447fae (5 days ago) • 2018-11-29 19:41:26 -0800
    Engine • revision 7375a0f414
    Tools • Dart 2.1.0 (build 2.1.0-dev.9.4 f9ebf21297)

img

由于我们做App,并不会1:1模仿,而是要符合App的使用规范,所以这里,我们将不同的栏目作为bootomBar。

这里我们先起一个flutter项目,命名为flutter_juejin

建立如下目录文件:

img

后面我们的目录大致修改为:

IMAGE

定义每一个页面class

  • pages/activityPage.dart
  import 'package:flutter/material.dart';
  
  class ActivityPage extends StatefulWidget {
    _ActivityPageState createState() => _ActivityPageState();
  }
  
  class _ActivityPageState extends State<ActivityPage> {
    @override
    Widget build(BuildContext context) {
      return Container(
         child: Text('活动'),
      );
    }
  }
  • pages/bookPage.dart
  import 'package:flutter/material.dart';
  
  class BookPage extends StatefulWidget {
    _BookPageState createState() => _BookPageState();
  }
  
  class _BookPageState extends State<BookPage> {
    @override
    Widget build(BuildContext context) {
      return  new DefaultTabController(
          length: 3,
          child: new Scaffold(
            appBar: new AppBar(
              backgroundColor: Colors.orangeAccent,
              title: new TabBar(
                tabs: [
                  new Tab(
                    child:new Column(
                      children:<Widget>[
                        new Icon(Icons.directions_car),
                        new Text('Car')
                      ]
                    )
                  ),
                  new Tab(
                    child:new Column(
                      children:<Widget>[
                        new Icon(Icons.directions_transit),
                        new Text('transit')
                      ]
                    )
                  ),
                  new Tab(
                    child:new Column(
                      children:<Widget>[
                        new Icon(Icons.directions_bike),
                        new Text('bike')
                      ]
                    )
                  ),
                ],
                indicatorColor: Colors.white,
              ),
            ),
            body: new TabBarView(
              children: [
                new Icon(Icons.directions_car),
                new Icon(Icons.directions_transit),
                new Icon(Icons.directions_bike),
              ],
            ),
          ),
        )
      ;
    }
  }

由于小册页面会有一个tab,我们暂时先拟定一个tab界面,后面再修改,你也可以暂时先像activityPage那样写个text上去

  • pages/indexPage.dart
  import 'package:flutter/material.dart';
  
  
   class IndexPage extends StatefulWidget {
     _IndexPageState createState() => _IndexPageState();
   }
   
   class _IndexPageState extends State<IndexPage> {
     @override
     Widget build(BuildContext context) {
       return Container(
          child: Text('IndexPage'),
       );
     }
   }
  • pages/pinsPage.dart
  import 'package:flutter/material.dart';
  
    class PinsPage extends StatefulWidget {
      _PinsPageState createState() => _PinsPageState();
    }
    
    class _PinsPageState extends State<PinsPage> {
      @override
      Widget build(BuildContext context) {
        return Container(
           child: Text('沸点'),
        );
      }
    }
  • pages/reposPage.dart
  import 'package:flutter/material.dart';
  
  class ReposPage extends StatefulWidget {
    _ReposPageState createState() => _ReposPageState();
  }
  
  class _ReposPageState extends State<ReposPage> {
    @override
    Widget build(BuildContext context) {
      return Container(
         child: Text('开源库'),
      );
    }
  }

然后我们在pages/myApp.dart中定义一个class,引入到main.dart中

  • pages/mian.dart
  import 'package:flutter/material.dart';
  import './pages/myApp.dart';
  
  void main() => runApp(MyApp());

编写入口文件 myApp.dart

  • pages/myApp.dart

为了方面后面扩展,我们这里使用StatefulWidget

  class MyApp extends StatefulWidget {
    _MyAppState createState() => _MyAppState();
  }
  class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
    
  }

在 State中,我们定义如下:

    final TextStyle tabTextStyleNormal =
        TextStyle(color: const Color(0xffdddddd));
    final TextStyle tabTextStyleSelected =
        TextStyle(color: const Color(0xff4d91fd));
    // 底部bar
    final List<Tab> _bottomTabs = <Tab>[
      Tab(
        text: '首页',
        icon: Icon(Icons.home),
      ),
      Tab(
        text: '沸点',
        icon: Icon(Icons.chat),
      ),
      Tab(
        text: '小册',
        icon: Icon(Icons.book),
      ),
      Tab(
        text: '开源库',
        icon: Icon(Icons.bubble_chart),
      ),
      Tab(
        text: '活动',
        icon: Icon(Icons.local_activity),
      ),
    ];
    var _body;
    List _appBarTitles = ['首页', '沸点', '小册', '开源库', '活动'];

上面定义了一些我们页面会使用到的一个变量,当然,页面顶部需要引入我们之前写好的一些页面

  import 'package:flutter/material.dart';
  
  import './indexPage.dart';
  import './pinsPage.dart';
  import './bookPage.dart';
  import './reposPage.dart';
  import './activityPage.dart';

bottomNavigationBar中我们需要使用TabBar,所以在定义变量的时候,别忘记了加一个Controller

这里说一下一个VSCode的小技巧,当鼠标悬停到Widget的时候,我们可以看到这个Widget都有哪些属性,
img

Controller其实也可以说不需要,因为他的作用就是定义底部bar的长度,加上它后,我们可以通过setState来改变底部的菜单按钮,更多的使用介绍,大家可以查看Flutter的api。

顺便说下对于必须的属性,编译器会给我们对应的提示的。后面我们会遇到。

这里我们添加Controller (万一后面我们会改变底部bar的长度呢,当然,能用StatelessWidget的最好用StatelessWidget,性能,你懂得)

  TabController _tabController;
  
  @override
    void initState() {
      // TODO: implement initState
      super.initState();
      _tabController =
          new TabController(vsync: this, length: _bottomTabs.length);
    }
  
    @override
    void dispose() {
      _tabController.dispose();
      super.dispose();
    }

基本准备工作都有了,然后我们来写我们的build方法吧

  @override
    Widget build(BuildContext context) {
      return Container(
        child: MaterialApp(
          theme: ThemeData(primaryColor: const Color.fromRGBO(77, 145, 253, 1.0)),
          home: Scaffold(
            appBar: AppBar(
              title: Text('Title'),
            ),
            body: TabBarView(
              controller: _tabController,
              children: <Widget>[
                IndexPage(),
                PinsPage(),
                BookPage(),
                ReposPage(),
                ActivityPage()
              ],
            ),
            bottomNavigationBar: new Material(
              color: Theme.of(context).primaryColor,
              child: TabBar(
                tabs: _bottomTabs,
                controller: _tabController,
                indicatorColor: Colors.white,
              ),
            ),
          ),
        ),
      );
    }

Scaffold 是Material提供的一个类似HTML的架构,其中包括header、body差不多

这次,我们app的基础结构就都已经搭建起来了。后面需要调整的时候我们再做相应调整,包括项目的目录结构后面肯定会调整的。

img

后面会介绍目录部分的跳转和添加

总结

这一节中,你应该学会如何

  • 定义新页面
  • 顶部tab
  • bottomNavigationBar
  • VSCode的一些使用技巧

这一节中,鼓励大家自己敲代码,代码地址将在下一节中给出。

登陆功能 & App响应

前言

截止目前,我们完成了基本的页面UI编写,毕竟作为一款社区app,登陆功能也是必须的。但是登陆功能对接我们不同业务可能场景不同,无论是存储session还是token或者uid,都是根绝自己业务需求来的。所以这里我们大致简单的模拟一下

登陆page

从掘金web版的开发api可以看出,登陆通过调用https://juejin.im/auth/type/phoneNumber ,通过post请求去发送我们账号、密码。但是这里我们并没有必要去模拟,毕竟不同公司,不同业务对于敏感数据的处理是不同的。

  • lib/pages/login.dart
  Column(
            children: <Widget>[
              TextField(
                keyboardType: TextInputType.text,
                decoration: InputDecoration(
                  contentPadding: const EdgeInsets.all(10.0),
                  icon: Icon(Icons.person),
                  labelText: '请输入用户名',
                ),
                onChanged: _userNameChange,
                autofocus: false,
              ),
              SizedBox(
                height: 20.0,
              ),
              TextField(
                keyboardType: TextInputType.text,
                decoration: InputDecoration(
                  contentPadding: const EdgeInsets.all(10.0),
                  icon: Icon(Icons.security),
                  labelText: '请输入密码',
                ),
                onChanged: _passwordChange,
                autofocus: false,
                obscureText: true,
              ),
              SizedBox(
                height: 20.0,
              ),
              FlatButton(
                onPressed: () {
                  if (_userName != '' && _password != '') {
                    Application.router.pop(context);
                    ApplicationEvent.event
                        .fire(UserLoginEvent(_userName,_userPic));
                  }
                },
                color: Theme.of(context).primaryColor,
                child: Text('登陆',
                    style: TextStyle(color: Colors.white, fontSize: 18.0)),
              )
            ],
          )
  • 使用TextField widget 来完成对输入框的编写 关于 TextField widget更多的使用可以参照 博客总结 也可以参照 官网文档
  • 点击button后,触发登陆请求,这里笔者只是简单模拟,更多的操作读者们也可以自己探索,根据自己的业务需求

App相应

这里我们使用 flutter package中的event_bus 目的在于有些页面,在登陆后和登录前UI展示是不一样的,所以这里,我们需要在登陆后,通过对应需要更改状态的页面修改相应的状态。

首先我们在项目中导入 event_bus 包。

然后修改项目整体结构:lib目录下添加event目录
img

在 event-bus.dart中配置EventBus

  import 'package:event_bus/event_bus.dart';
  
  class ApplicationEvent{
    static EventBus event;
  }

在 event-model.dart 中配置项目所需的全部事件元

  class UserLoginEvent{
    final String userName;
    final String userPic;
    // token uid...
    UserLoginEvent(this.userName,this.userPic);
  }

这里我们就定义了一个 UserLoginEvent class

然后在入口文件中,实例化 event_bus

  • lib/pages/my_app.dart
  String _userName = '';
  String _userPic = '';
  ...
  _MyAppState() {
    final router = new Router();
    final eventBus = new EventBus();
    Routes.configureRoutes(router);
    Application.router = router;
    ApplicationEvent.event = eventBus;
  }

然后在页面的 initState 中注入相关的事件监听


  @override
  void initState() {
    super.initState();
    _tabController = new TabController(vsync: this, length: _bottomTabs.length);
    ApplicationEvent.event.on<UserLoginEvent>().listen((event) {
      setState(() {
        _userName = event.userName;
        _userPic = event.userPic;
      });
    });
  }

完整代码地址:my_app.dart

发送广播

在我们登陆页面,登陆后,即可发送对应的广播

                onPressed: () {
                  if (_userName != '' && _password != '') {
                    ApplicationEvent.event
                        .fire(UserLoginEvent(_userName,_userPic));
                    Application.router.pop(context);
                  }
                },

效果如下:
login

总结

如上我们就完成了app的mock 登陆以及广播的发送,其实对于Flutter中,我们也可以使用类似react中的react-redux来统一管理我们的state。Flutter redux 具体实现可以自行查阅相关文档。关于登陆响应当然也有更多的实现方式,欢迎大家评论探讨指教~ 当然event_bus还是一个非常实用的package,也希望大家能够多多利用。利用的好,必然会给我们带来开发上的很多便利。

Flutter 状态管理之 Scoped Model & Redux

前言

文章原文地址:Nealyang/PersonalBlog

可能作为一个前端,在学习 Flutter 的过程中,总感觉非常非常相似 React Native,甚至于,其中还是有state的概念 setState,所以在 Flutter 中,也当然会存在非常多的解决方案,比如 redux 、RxDart 还有 Scoped Model等解决方案。今天,我们主要介绍下常用的两种 State 管理解决方案:redux、scoped model。

Scoped Model

介绍

Scoped Model 是 package 上 Dart 的一个第三方库scoped_model。Scoped Model 主要是通过数据model的概念来实现数据传递,表现上类似于 react 中 context 的概念。它提供了让子代widget轻松获取父级数据model的功能。

从官网中的介绍可以了解到,它直接来自于Google正在开发的新系统Fuchsia核心 Widgets 中对 Model 类的简单提取,作为独立使用的独立 Flutter 插件发布。

在直接上手之前,我们先着重说一下 Scoped Model 中几个重要的概念

  • Model 类,通过继承 Model 类来创建自己的数据 model,例如 SearchModel 或者 UserModel ,并且还可以监听 数据model的变化
  • ScopedModelDescendant widget , 如果你需要传递数据 model 到很深层级里面的 widget ,那么你就需要用 ScopedModel 来包裹 Model,这样的话,后面所有的子widget 都可以使用该数据 model 了(是不是更有一种 context 的感觉)
  • ScopedModelDescendant widget ,使用此 widget 可以在 widget tree 中找到相应的 Scope的Model ,当 数据 model 发生变化的时候,该 widget 会重新构建

当然,在 Scoped Model 的文档中,也介绍了一些 实现原理

  • Model类实现了Listenable接口
    • AnimationController和TextEditingController也是Listenables
  • 使用InheritedWidget将数据 model 传递到Widget树。 重建 InheritedWidget 时,它将手动重建依赖于其数据的所有Widgets。 无需管理订阅!
  • 它使用 AnimatedBuilder Widget来监听Model并在模型更改时重建InheritedWidget

实操Demo

demo地址

img

从gif上可以看到咱们的需求非常的简单,就是在当前页面更新了count后,在第二个页面也能够传递过去。当然,new ResultPage(count:count)就没意思啦~ 咱不讨论哈

新建数据 model

lib/model/counter_model.dart

  import 'package:scoped_model/scoped_model.dart';
  
  class CounterModel extends Model{
  
    int _counter = 0;
  
    int get counter => _counter;
  
    void increment(){
  
      _counter++;
  
      // 通知所有的 listener
      notifyListeners();
    }
  }
  • 这一步非常的简单,新建一个类去继承 Model
  • 里面定义了一个 get方法,以便于后面取数据model
  • 定义了 increment 方法,去改变我们的数据 model ,调用 package 中的 通知方法 notifyListeners

lib/main.dart

  import 'package:flutter/material.dart';
  import './model/counter_model.dart';
  import 'package:scoped_model/scoped_model.dart';
  import './count_page.dart';
  
  void main() {
    runApp(MyApp(
      model: CounterModel(),
    ));
  }
  
  class MyApp extends StatelessWidget {
  
    final CounterModel model;
  
    const MyApp({Key key,@required this.model}):super(key:key);
  
    @override
    Widget build(BuildContext context) {
      return ScopedModel(
        model: model,
        child: MaterialApp(
          title: 'Scoped Model Demo',
          home:CountPage(),
        ),
      );
    }
  }

这是 app 的入口文件,划重点

  • MyApp 类 中,我们传入一个定义好的数据 model ,方便后面传递给子类
  • MaterialAppScopedModel 包裹一下,作用上面已经介绍了,方便子类可以拿到 ,类似于 reduxProvider 包裹一下
  • 一定需要将数据 model 传递给 ScopedModel 的 model 属性中

lib/count_page.dart

  class CountPage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Scoped Model'),
          actions: <Widget>[
            IconButton(
              tooltip: 'to result',
              icon: Icon(Icons.home),
              onPressed: (){
                Navigator.push(context,MaterialPageRoute(builder: (context)=>ResultPage()));
              },
            )
          ],
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('你都点击'),
              ScopedModelDescendant<CounterModel>(
                builder: (context, child, model) {
                  return Text(
                    '${model.counter.toString()} 次了',
                    style: TextStyle(
                      color: Colors.red,
                      fontSize: 33.0,
                    ),
                  );
                },
              )
            ],
          ),
        ),
        floatingActionButton: ScopedModelDescendant<CounterModel>(
          builder: (context,child,model){
            return FloatingActionButton(
              onPressed: model.increment,
              tooltip: 'add',
              child: Icon(Icons.add),
            );
          },
        ),
      );
    }
  }

常规布局和widget这里不再重复介绍,我们说下主角:Scoped Model

  • 简单一句,哪里需要用数据 model ,哪里就需要用 ScopedModelDescendant
  • ScopedModelDescendant中的build方法需要返回一个widget,在这个widget中我们可以使用数据 model中的方法、数据等

最后在 lib/result_page.dart中就可以看到我们数据 model 中的 count 值了,注意这里跳转页面,我们并没有通过参数传递的形式传递 Navigator.push(context,MaterialPageRoute(builder: (context)=>ResultPage()));

完整项目代码:flutter_scoped_model

flutter_redux

相信作为一个前端对于 redux 一定不会陌生,而 Flutter 中也同样存在 state 的概念,其实说白了,UI 只是数据(state)的另一种展现形式。study-redux是笔者之前学习redux时候的一些笔记和心得。这里为了防止有新人不太清楚redux,我们再来介绍下redux的一些基本概念

state

state 我们可以理解为前端UI的状态(数据)库,它存储着这个应用所有需要的数据。
img

action

既然这些state已经有了,那么我们是如何实现管理这些state中的数据的呢,当然,这里就要说到action了。 什么是action?E:action:动作。 是的,就是这么简单。。。

只有当某一个动作发生的时候才能够触发这个state去改变,那么,触发state变化的原因那么多,比如这里的我们的点击事件,还有网络请求,页面进入,鼠标移入。。。所以action的出现,就是为了把这些操作所产生或者改变的数据从应用传到store中的有效载荷。 需要说明的是,action是state的唯一信号来源。

reducer

reducer决定了state的最终格式。 reducer是一个纯函数,也就是说,只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。reducer对传入的action进行判断,然后返回一个通过判断后的state,这就是reducer的全部职责。 从代码可以简单地看出:

  import {INCREMENT_COUNTER,DECREMENT_COUNTER} from '../actions';
  
  export default function counter(state = 0,action) {
      switch (action.type){
          case INCREMENT_COUNTER:
              return state+1;
          case DECREMENT_COUNTER:
              return state-1;
          default:
              return state;
      }
  }

对于一个比较大一点的应用来说,我们是需要将reducer拆分的,最后通过redux提供的combineReducers方法组合到一起。 比如:

  const rootReducer = combineReducers({
      counter
  });
  
  export default rootReducer;

这里你要明白:每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。 combineReducers() 所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理, 然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。

store

store是对之前说到一个联系和管理。具有如下职责

  • 维持应用的 state;
  • 提供 getState() 方法获取 state
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。

再次强调一下 Redux 应用只有一个单一的 store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。 store的创建通过redux的createStore方法创建,这个方法还需要传入reducer,很容易理解:毕竟我需要dispatch一个action来改变state嘛。 应用一般会有一个初始化的state,所以可选为第二个参数,这个参数通常是有服务端提供的,传说中的Universal渲染。后面会说。。。 第三个参数一般是需要使用的中间件,通过applyMiddleware传入。

说了这么多,action,store,action creator,reducer关系就是这么如下的简单明了:

img

结合 flutter_redux

一些工具集让你轻松地使用 redux 来轻松构建 Flutter widget,版本要求是 redux.dart 3.0.0+

Redux Widgets

  • StoreProvider :基础组件,它将给定的 Redux Store 传递给所欲请求它的的子代组件
  • StoreBuilder : 一个子代组件,它从 StoreProvider 获取 Store 并将其传递给 widget 的 builder 方法中
  • StoreConnector :获取 Store 的一个子代组件

StoreProvider ancestor,使用给定的 converter 函数将 Store 转换为 ViewModel ,并将ViewModel传递给 builder。 只要 Store 发出更改事件(action),Widget就会自动重建。 无需管理订阅!

注意

Dart 2需要更严格的类型!

1、确认你正使用的是 redux 3.0.0+
2、在你的组件树中,将 new StoreProvider(...) 改为 new StoreProvider<StateClass>(...)
3、如果需要从StoreProvider<AppState> 中直接获取 Store<AppState> ,则需要将 new StoreProvider.of(context) 改为 StoreProvider.of<StateClass> .不需要直接访问 Store 中的字段,因为Dart2可以使用静态函数推断出正确的类型

实操演练

官方demo的代码先大概解释一下

  import 'package:flutter/material.dart';
  import 'package:flutter_redux/flutter_redux.dart';
  import 'package:redux/redux.dart';
  
  //定义一个action: Increment
  enum Actions { Increment }
  
  // 定义一个 reducer,响应传进来的 action
  int counterReducer(int state, dynamic action) {
    if (action == Actions.Increment) {
      return state + 1;
    }
  
    return state;
  }
  
  void main() {
    // 在 基础 widget 中创建一个 store,用final关键字修饰  这比直接在build方法中创建要好很多
    final store = new Store<int>(counterReducer, initialState: 0);
  
    runApp(new FlutterReduxApp(
      title: 'Flutter Redux Demo',
      store: store,
    ));
  }
  
  class FlutterReduxApp extends StatelessWidget {
    final Store<int> store;
    final String title;
  
    FlutterReduxApp({Key key, this.store, this.title}) : super(key: key);
  
    @override
    Widget build(BuildContext context) {
      // 用  StoreProvider 来包裹你的 MaterialApp 或者别的 widget ,这样能够确保下面所有的widget能够获取到store中的数据
      return new StoreProvider<int>(
        // 将 store  传递给 StoreProvider
        // Widgets 将使用 store 变量来使用它
        store: store,
        child: new MaterialApp(
          theme: new ThemeData.dark(),
          title: title,
          home: new Scaffold(
            appBar: new AppBar(
              title: new Text(title),
            ),
            body: new Center(
              child: new Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  new Text(
                    'You have pushed the button this many times:',
                  ),
                  // 通过 StoreConnector 将 store 和 Text 连接起来,以便于 Text直接render
                  // store 中的值。类似于 react-redux 中的connect
                  //
                  // 将 Text widget 包裹在 StoreConnector 中,
                  // `StoreConnector`将会在最近的一个祖先元素中找到 StoreProvider
                  // 拿到对应的值,然后传递给build函数
                  //
                  // 每次点击按钮的时候,将会 dispatch 一个 action并且被reducer所接受。
                  // 等reducer处理得出最新结果后, widget将会自动重建
                  new StoreConnector<int, String>(
                    converter: (store) => store.state.toString(),
                    builder: (context, count) {
                      return new Text(
                        count,
                        style: Theme.of(context).textTheme.display1,
                      );
                    },
                  )
                ],
              ),
            ),
            // 同样使用 StoreConnector 来连接Store 和FloatingActionButton
            // 在这个demo中,我们使用store 去构建一个包含dispatch、Increment 
            // action的回调函数
            //
            // 将这个回调函数丢给 onPressed
            floatingActionButton: new StoreConnector<int, VoidCallback>(
              converter: (store) {
                return () => store.dispatch(Actions.Increment);
              },
              builder: (context, callback) {
                return new FloatingActionButton(
                  onPressed: callback,
                  tooltip: 'Increment',
                  child: new Icon(Icons.add),
                );
              },
            ),
          ),
        ),
      );
    }
  }

上面的例子比较简单,鉴于小册Flutter入门实战:从0到1仿写web版掘金App下面有哥们在登陆那块评论了Flutter状态管理,

这里我简单使用redux模拟了一个登陆的demo

img

lib/reducer/reducers.dart

首先我们定义action需要的一些action type

  enum Actions{
    Login,
    LoginSuccess,
    LogoutSuccess
  }

然后定义相应的类来管理登陆状态

  class AuthState{
    bool isLogin;     //是否登录
    String account;   //用户名
    AuthState({this.isLogin:false,this.account});
  
    @override
    String toString() {
      return "{account:$account,isLogin:$isLogin}";
    }
  }

然后我们需要定义一些action,定义个基类,然后定义登陆成功的action

  class Action{
    final Actions type;
    Action({this.type});
  }
  
  class LoginSuccessAction extends Action{
  
    final String account;
  
    LoginSuccessAction({
      this.account
    }):super( type:Actions.LoginSuccess );
  }

最后定义 AppState 以及我们自定义的一个中间件。

  // 应用程序状态
  class AppState {
    AuthState auth; //登录
    MainPageState main; //主页
  
    AppState({this.main, this.auth});
  
    @override
    String toString() {
      return "{auth:$auth,main:$main}";
    }
  }
  
  AppState mainReducer(AppState state, dynamic action) {
  
    if (Actions.LogoutSuccess == action) {
      state.auth.isLogin = false;
      state.auth.account = null;
    }
  
    if (action is LoginSuccessAction) {
      state.auth.isLogin = true;
      state.auth.account = action.account;
    }
  
    print("state changed:$state");
  
    return state;
  }
  
  loggingMiddleware(Store<AppState> store, action, NextDispatcher next) {
    print('${new DateTime.now()}: $action');
  
    next(action);
  }

在稍微大一点的项目中,其实就是reducer 、 state 和 action 的组织会比较麻烦,当然,罗马也不是一日建成的, 庞大的state也是一点一点累计起来的。

下面就是在入口文件中使用 redux 的代码了,跟基础demo没有差异。

  import 'package:flutter/material.dart';
  import 'package:flutter_redux/flutter_redux.dart';
  import 'package:redux/redux.dart';
  import 'dart:async' as Async;
  import './reducer/reducers.dart';
  import './login_page.dart';
  
  void main() {
    Store<AppState> store = Store<AppState>(mainReducer,
        initialState: AppState(
          main: MainPageState(),
          auth: AuthState(),
        ),
        middleware: [loggingMiddleware]);
  
    runApp(new MyApp(
      store: store,
    ));
  }
  
  class MyApp extends StatelessWidget {
    final Store<AppState> store;
  
    MyApp({Key key, this.store}) : super(key: key);
  
    @override
    Widget build(BuildContext context) {
      return new StoreProvider(store: store, child: new MaterialApp(
        title: 'Flutter Demo',
        theme: new ThemeData(
          primarySwatch: Colors.blue,
        ),
        home:  new StoreConnector<AppState,AppState>(builder: (BuildContext context,AppState state){
          print("isLogin:${state.auth.isLogin}");
          return new MyHomePage(title: 'Flutter Demo Home Page',
            counter:state.main.counter,
              isLogin: state.auth.isLogin,
              account:state.auth.account);
        }, converter: (Store<AppState> store){
          return store.state;
        }) ,
        routes: {
          "login":(BuildContext context)=>new StoreConnector(builder: ( BuildContext context,Store<AppState> store ){
  
            return new LoginPage(callLogin: (String account,String pwd) async{
              print("正在登录,账号$account,密码:$pwd");
              // 为了模拟实际登录,这里等待一秒
              await new Async.Future.delayed(new Duration(milliseconds: 1000));
              if(pwd != "123456"){
                throw ("登录失败,密码必须是123456");
              }
              print("登录成功!");
              store.dispatch(new LoginSuccessAction(account: account));
  
            },);
          }, converter: (Store<AppState> store){
            return store;
          }),
  
        },
      ));
    }
  }
  
  
  
  
  class MyHomePage extends StatelessWidget {
    MyHomePage({Key key, this.title, this.counter, this.isLogin, this.account})
        : super(key: key);
    final String title;
    final int counter;
    final bool isLogin;
    final String account;
  
    @override
    Widget build(BuildContext context) {
      print("build:$isLogin");
      Widget loginPane;
      if (isLogin) {
        loginPane = new StoreConnector(
            key: new ValueKey("login"),
            builder: (BuildContext context, VoidCallback logout) {
              return new RaisedButton(
                onPressed: logout, child: new Text("您好:$account,点击退出"),);
            }, converter: (Store<AppState> store) {
          return () =>
              store.dispatch(
                  Actions.LogoutSuccess
              );
        });
      } else {
        loginPane = new RaisedButton(onPressed: () {
          Navigator.of(context).pushNamed("login");
        }, child: new Text("登录"),);
      }
      return new Scaffold(
        appBar: new AppBar(
          title: new Text(title),
        ),
        body: new Center(
          child: new Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
  
              /// 有登录,展示你好:xxx,没登录,展示登录按钮
              loginPane
            ],
          ),
        ),
  
      );
    }
  }

完整项目代码:Nealyang/Flutter

Flutter Go

更多学习 Flutter的小伙伴,欢迎入QQ群 Flutter Go :679476515

关于 Flutter 组件以及更多的学习,敬请关注我们正在开发的: alibaba/flutter-go

git

参考

flutter_architecture_samples
flutter_redux
flutter example
scoped_model

Decorator:从原理到实践,我一点都不虚~

前言

原文链接:Nealyang/personalBlog

img

ES6 已经不必在过多介绍,在 ES6 之前,装饰器可能并没有那么重要,因为你只需要加一层 wrapper 就好了,但是现在,由于语法糖 class 的出现,当我们想要去在多个类之间共享或者扩展一些方法的时候,代码会变得错综复杂,难以维护,而这,也正式我们 Decorator 的用武之地。

Object.defineProperty

关于 Object.defineProperty 简单的说,就是该方法可以精准的添加和修改对象的属性

语法

Object.defineProperty(obj,prop,descriptor)

  • ojb:要在其上定义属性的对象
  • prop:要定义或修改的属性的名称
  • descriptor:将被定义或修改的属性描述符

该方法返回被传递给函数的对象

在ES6中,由于 Symbol类型的特殊性,用Symbol类型的值来做对象的key与常规的定义或修改不同,而Object.defineProperty 是定义key为Symbol的属性的方法之一。

通过赋值操作添加的普通属性是可枚举的,能够在属性枚举期间呈现出来(for...in 或 Object.keys 方法), 这些属性的值可以被改变,也可以被删除。这个方法允许修改默认的额外选项(或配置)。默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改的

属相描述符

对象里目前存在的属性描述符有两种主要形式:数据描述符存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。

数据描述符和存取描述符均具有以下可选键值:

configurable

当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false

enumerable

当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。

数据描述符同时具有以下可选键值:

value

该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。

writable

当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false

存取描述符同时具有以下可选键值:

get

一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。默认为 undefined。

set

一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined。

如果一个描述符不具有value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常

更多使用实例和介绍,参看:MDN

装饰者模式

在看Decorator之前,我们先看下装饰者模式的使用,我们都知道,装饰者模式能够在不改变对象自身基础上,在程序运行期间给对象添加指责。特点就是不影响之前对象的特性,而新增额外的职责功能。

like...this:

IMAGE

这段比较简单,直接看代码吧:

let Monkey = function () {}
Monkey.prototype.say = function () {
  console.log('目前我只是个野猴子');
}
let TensionMonkey = function (monkey) {
  this.monkey = monkey;
}
TensionMonkey.prototype.say = function () {
  this.monkey.say();
  console.log('带上紧箍咒,我就要忘记世间烦恼!');
}
let monkey = new TensionMonkey(new Monkey());
monkey.say();

执行结果:
IMAGE

Decorator

Decorator其实就是一个语法糖,背后其实就是利用es5的Object.defineProperty(target,name,descriptor),了解Object.defineProperty请移步这个链接:MDN文档

其背后原理大致如下:

class Monkey{
  say(){
    console.log('目前,我只是个野猴子');
  }
}

执行上面的代码,大致代码如下:

Object.defineProperty(Monkey.prototype,'say',{
  value:function(){console.log('目前,我只是个野猴子')},
  enumerable:false,
  configurable:true,
  writable:true
})

如果我们利用装饰器来修饰他

class Monkey{
@readonly
say(){console.log('现在我是只读的了')}
}

在这种装饰器的属性,会在Object.defineProperty为Monkey.prototype注册say属性之前,执行以下代码:

let descriptor = {
  value:specifiedFunction,
  enumerable:false,
  configurable:true,
  writeable:true
};

descriptor = readonly(Monkey.prototype,'say',descriptor)||descriptor;
Object.defineProperty(Monkey.prototype,'say',descriptor);

从上面的伪代码我们可以看出,Decorator只是在Object.defineProperty为Monkey.prototype注册属性之前,执行了一个装饰函数,其属于一个类对Object.defineProperty的拦截。所以它和Object.defineProperty具有一致的形参:

  • obj:作用的目标对象
  • prop:作用的属性名
  • descriptor:针对该属性的描述符

下面看下简单的使用

在class中的使用

  • 创建一个新的class继承自原有的class,并添加属性
@name
class Person{
  sayHello(){
    console.log(`hello ,my name is ${this.name}`)
  }
}

function name(constructor) {  
  return class extends constructor{
    name="Nealyang"
  }
}

new Person().sayHello()
//hello ,my name is Nealyang
  • 针对当前class修改(类似mixin)
@name
@seal
class Person {
  sayHello() {
    console.log(`hello ,my name is ${this.name}`)
  }
}

function name(constructor) {
  Object.defineProperty(constructor.prototype,'name',{
    value:'一凨'
  })
}
new Person().sayHello()

//若修改一个属性

function seal(constructor) {
  let descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, 'sayHello')
  Object.defineProperty(constructor.prototype, 'sayHello', {
    ...descriptor,
    writable: false
  })
}

new Person().sayHello = 1;// Cannot assign to read only property 'sayHello' of object '#<Person>'

上面说到mixin,那么我就来模拟一个mixin吧

class A {
  run() {
    console.log('我会跑步!')
  }
}

class B {
  jump() {
    console.log('我会跳!')
  }
}

@mixin(A, B)
class C {}

function mixin(...args) {
  return function (constructor) {
    for (const arg of args) {
      for (let key of Object.getOwnPropertyNames(arg.prototype)) {
        if (key === 'constructor') continue;
        Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key));
      }
    }
  }
}

let c = new C();
c.jump();
c.run();
// 我会跳!
// 我会跑步!

截止目前我們貌似写了非常多的代码了,对。。。这篇,为了彻底搞投Decorator,这。。。只是开始。。。

img

在class成员中的使用

这类的装饰器的写法应该就是我们最为熟知了,会接受三个参数:

  • 如果装饰器挂载在静态成员上,则会返回构造函数,如果挂载在实例成员上,则返回类的原型
  • 装饰器挂载的成员名称
  • Object.getOwnPropertyDescriptor的返回值

首先,我们明确下静态成员和实例成员的区别

class Model{
  //实例成员
  method1(){}
  method2 = ()=>{}
  
  // 靜態成員
  static method3(){}
  static method4 = ()=>{}
}

method1 和method2 是实例成员,但是method1存在于prototype上,method2只有实例化对象以后才有。

method3和method4是静态成员,两者的区别在于是否可枚举描述符的设置,我们通过babel转码可以看到:

IMAGE

上述代码比较乱,简单的可以理解为:

function Model () {
  // 成员仅在实例化时赋值
  this.method2 = function () {}
}

// 成员被定义在原型链上
Object.defineProperty(Model.prototype, 'method1', {
  value: function () {}, 
  writable: true, 
  enumerable: false,  // 设置不可被枚举
  configurable: true
})

// 成员被定义在构造函数上,且是默认的可被枚举
Model.method4 = function () {}

// 成员被定义在构造函数上
Object.defineProperty(Model, 'method3', {
  value: function () {}, 
  writable: true, 
  enumerable: false,  // 设置不可被枚举
  configurable: true
})

可以看出,只有method2是在实例化时才赋值的,一个不存在的属性是不会有descriptor的,所以这就是为什么在针对Property Decorator不传递第三个参数的原因,至于为什么静态成员也没有传递descriptor,目前没有找到合理的解释,但是如果明确的要使用,是可以手动获取的。

就像上述的示例,我们针对四个成员都添加了装饰器以后,method1和method2第一个参数就是Model.prototype,而method3和method4的第一个参数就是Model。

class Model {
  // 实例成员
  @instance
  method1 () {}
  @instance
  method2 = () => {}

  // 静态成员
  @static
  static method3 () {}
  @static
  static method4 = () => {}
}

function instance(target) {
  console.log(target.constructor === Model)
}

function static(target) {
  console.log(target === Model)
}

函数、访问器、属性 三者装饰器的使用

  • 函数装饰器的返回值会默认作为属性的value描述符的存在,如果返回为undefined则忽略
class Model {
  @log1
  getData1() {}
  @log2
  getData2() {}
}

// 方案一,返回新的value描述符
function log1(tag, name, descriptor) {
  return {
    ...descriptor,
    value(...args) {
      let start = new Date().valueOf()
      try {
        return descriptor.value.apply(this, args)
      } finally {
        let end = new Date().valueOf()
        console.log(`start: ${start} end: ${end} consume: ${end - start}`)
      }
    }
  }
}

// 方案二、修改现有描述符
function log2(tag, name, descriptor) {
  let func = descriptor.value // 先获取之前的函数

  // 修改对应的value
  descriptor.value = function (...args) {
    let start = new Date().valueOf()
    try {
      return func.apply(this, args)
    } finally {
      let end = new Date().valueOf()
      console.log(`start: ${start} end: ${end} consume: ${end - start}`)
    }
  }
}
  • 访问器的Decorator就是get set前缀函数了,用于控制属性的赋值、取值操作,在使用上和函数装饰器没有任何区别

class Modal {
  _name = 'Niko'

  @prefix
  get name() { return this._name }
}

function prefix(target, name, descriptor) {
  return {
    ...descriptor,
    get () {
      return `wrap_${this._name}`
    }
  }
}

console.log(new Modal().name) // wrap_Niko
  • 对于属性装饰器是没有descriptor返回的,并且装饰器函数的返回值也会被忽略,如果我们需要修改某一个静态属性,则需要自己获取descriptor
class Modal {
  @prefix
  static name1 = 'Niko'
}

function prefix(target, name) {
  let descriptor = Object.getOwnPropertyDescriptor(target, name)

  Object.defineProperty(target, name, {
    ...descriptor,
    value: `wrap_${descriptor.value}`
  })
}

console.log(Modal.name1) // wrap_Niko

对于一个实例的属性,则没有直接修改的方案,不过我们可以结合着一些其他装饰器来曲线救国。

比如,我们有一个类,会传入姓名和年龄作为初始化的参数,然后我们要针对这两个参数设置对应的格式校验

const validateConf = {} // 存储校验信息

@validator
class Person {
  @validate('string')
  name
  @validate('number')
  age

  constructor(name, age) {
    this.name = name
    this.age = age
  }
}

function validator(constructor) {
  return class extends constructor {
    constructor(...args) {
      super(...args)

      // 遍历所有的校验信息进行验证
      for (let [key, type] of Object.entries(validateConf)) {
        if (typeof this[key] !== type) throw new Error(`${key} must be ${type}`)
      }
    }
  }
}

function validate(type) {
  return function (target, name, descriptor) {
    // 向全局对象中传入要校验的属性名及类型
    validateConf[name] = type
  }
}

new Person('Niko', '18')  // throw new error: [age must be number]

函数参数装饰器

const parseConf = {}
class Modal {
  @parseFunc
  addOne(@parse('number') num) {
    return num + 1
  }
}

// 在函数调用前执行格式化操作
function parseFunc (target, name, descriptor) {
  return {
    ...descriptor,
    value (...arg) {
      // 获取格式化配置
      for (let [index, type] of parseConf) {
        switch (type) {
          case 'number':  arg[index] = Number(arg[index])             break
          case 'string':  arg[index] = String(arg[index])             break
          case 'boolean': arg[index] = String(arg[index]) === 'true'  break
        }

        return descriptor.value.apply(this, arg)
      }
    }
  }
}

// 向全局对象中添加对应的格式化信息
function parse(type) {
  return function (target, name, index) {
    parseConf[index] = type
  }
}

console.log(new Modal().addOne('10')) // 11

Decorator 用例

img

log

为一个方法添加 log 函数,检查输入的参数

    let log = type => {
      return (target,name,decorator) => {
        const method = decorator.value;
        console.log(method);

        decorator.value = (...args) => {
          console.info(`${type} 正在进行:${name}(${args}) = ?`);
          let result;
          try{
            result = method.apply(target,args);
            console.info(`(${type}) 成功 : ${name}(${args}) => ${result}`);
          }catch(err){
            console.error(`(${type}) 失败: ${name}(${args}) => ${err}`);
          }
          return result;
        }
      }
    }

    class Math {
      @log('add')
      add(a, b) {
        return a + b;
      }
    }

    const math = new Math();

    // (add) 成功 : add(2,4) => 6
    math.add(2, 4);

img

time

用于统计方法执行的时间:

function time(prefix) {
  let count = 0;
  return function handleDescriptor(target, key, descriptor) {

    const fn = descriptor.value;

    if (prefix == null) {
      prefix = `${target.constructor.name}.${key}`;
    }

    if (typeof fn !== 'function') {
      throw new SyntaxError(`@time can only be used on functions, not: ${fn}`);
    }

    return {
      ...descriptor,
      value() {
        const label = `${prefix}-${count}`;
        count++;
        console.time(label);

        try {
          return fn.apply(this, arguments);
        } finally {
          console.timeEnd(label);
        }
      }
    }
  }
}

debounce

对执行的方法进行防抖处理

class Toggle extends React.Component {

  @debounce(500, true)
  handleClick() {
    console.log('toggle')
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        button
      </button>
    );
  }
}

function _debounce(func, wait, immediate) {

    var timeout;

    return function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}

function debounce(wait, immediate) {
  return function handleDescriptor(target, key, descriptor) {
    const callback = descriptor.value;

    if (typeof callback !== 'function') {
      throw new SyntaxError('Only functions can be debounced');
    }

    var fn = _debounce(callback, wait, immediate)

    return {
      ...descriptor,
      value() {
        fn()
      }
    };
  }
}

更多关于 core-decorators 的例子后面再 Nealyang/PersonalBlog中补充,再加注释说明。

参考

学习方法分享:为何一年半就能拿到大厂 offer

都是运气!

毕竟是聊聊曾经,放一张大学课堂上灵光一现,手写的一个我曾经一直使用的网名

前言

讲真,的确是运气,才有机会进大厂。也没想到,那篇一年半工作经验试水杭州大厂的面经如此受欢迎。后面也有很多朋友在群里问我,你是如何学习的?

此篇为xxx 经验进阿里的终结篇,希望从此以后就翻过了,不再提了。不然总有种炫耀的感觉,倍感压力,汗颜汗颜~

此篇也并非技术软文。大概介绍下我在进阿里之前、工作中都经历和做过了些什么,最后我会分享一下敲开面试之门的那封简历。

关键节点经历交代

经历阶段,我尽量简短。

大学期间

从农村走出来的孩子,从只开开机关机到各种参加比赛,鬼知道我如何了解编程的。

最终我拿过Oracle java 全国青年设计大赛东北赛区一等奖、蓝桥杯编程省一、国三等等四五个编程方面的奖项吧。

大四实习期间

我使用 java 编写的坦克大战自定义 hack 版推开的实习公司汉得的门。在移动部,我原以为是用 java,结果是 hybrid App 开发。所以,实习期间,是我开始学习前端的开始。

刚开始的学习过程大家都一样,w3c搞起,最终,我成为了团队里面第一个带新人的,我还带人开发了现在汇联易App 的第一版。90%代码是我一个人写的。也理所应当的拿到了年度最佳新人奖项。这里再次感谢当初给我机会的我的老大,顺哥。

但是说实话,当时的技术,前端基础都掌握的不行、都是在用 ionic、cordova、angular。甚至连 jQuery 都不会。。。

第一份工作

毕业后在北京,第一份工作在环球网,事实证明当初的选择是正确的。

我正式接触前端,从编写页面开始。从刚开始的添加一个 click 事件监听都要百度,到最后半天能产出一张活动页。

后面一周学习 RN ,扛下了独自环球网 App (Android 版)的大旗。再后面调到平台组,开始接触了 react、node。

遇到过一些很多难的项目,也是当时我说的,怎么我一直在坑中。而这最后,都成为了我简历中比较出彩的地方。

阿里

后面决定跳槽,就想择一城。来到杭州,也就是大家看到的一年半前端工作经验试水杭州:我是如何拿下网易、阿里和滴滴 offer 的

关于前端

以下所有言论都是个人观点。如有不妥欢迎指出,一起交流

就前端而言,我个人认为有三个阶段。认知阶段、钻研阶段、掌握阶段

认知阶段

所谓认知阶段,就是开始接触前端,开始学习前端。

学习方法

这个阶段应该算是我在实习的阶段吧。一个从来接触过前端的大学生。简单总结就是各种看书、学习。

  • 从最基础的 HTML、css、JavaScript 开始学习。我个人是从 w3c 开始学习的,然后还顺带做了在线的知识掌握测试。
  • 每一次的工作都是挑战,每一次挑战都是成长。也是从这个时候,我开始养成了写博客的习惯。
  • 遇到任何新的技术,都从官网开始学习。因为这个阶段,官网能帮你解决 99%的问题
  • 遇到问题,尽量靠自己,别动不动就在群里提问。甚至,你要主动找问题。偷偷告诉你,我的 qq 群、微信群,都是我在刚学习这类知识的时候创建的,初期我是尽可能的回答群里每一个问题。虽然我是菜鸟,但是我会百度、Google 呀!

截止到 16 年初。这是我在实习阶段整理总结的自己项目中遇到的问题

开源** Nealyang 邪气小生

钻研阶段

所谓钻研阶段,就是你基本已经入门前端了,需要找一个方向,去学习,去钻研。比如三大框架是否可以挑选一门入坑。注意是钻研,而不是浅尝辄止。

我的学习方法

在这个阶段,我依旧会浏览各个官网的信息,同时就我个人而言,当初选择的是 react 技术栈+node ,这也是我最开始创建的两个技术交流群。

当然,工作中,恰巧我也用了一周时间学习 React Native,完成了官方 App 的代码编写。这让我提前对 react 有了一些了解。掌握 react 技术栈对于一个初学者来说挺艰难的。我花了一周,看完了所有教程。然后开始学习 react-router、redux、react-redux、然后也接触到了 webpack,在此之前,我刚学习 gulp(开源**博客列表可见相关总结)。

我的学习方法比较剑走偏锋。既然看完了知识点,直接开干。

  • 大概花了四天时间看了 nodejs 的基础知识,我写了一个 demo:ejs-express-mysql
  • webpack 学习完阮一峰的 demo 后我也开始百度、Google,完成一个自己项目的配置:neal-teach-website
  • redux 我是通宵学习了一个周末,并且在周一写了一些 demo、写了相关感悟study-redux
  • 然后开始将react 技术栈串联起来的时候,发现了 redux-saga 要学习,并且整体项目结构非常的乱。于是乎,我又开源了一个 demo:React-Fullstack-Dianping-Demo ,这是一个朋友分享给我的慕课网教学视频,但是说实话,通篇看完,觉得老师讲解的不是很对口,遂自己写了一个开源出来。
  • 通篇学习完后,又写了一个总结性的Demo,也就是 github 上目前个人仓库下最高 star :React-Express-Blog-Demo

以上这些只是我个人学习 react 的时候,并且所有的学习都有相关产出、所有的 demo 都在 github 可见。同时在工作中,也有在使用和学习。

回头看看,我一直在冒充着大神,其实开源出来的时候,自己也在学习,自己也没有完全掌握。因为我感觉如果都是写一些自己会的,那简直是太浪费时间了。

除了 react 以外。在这个阶段,我 啃完了所有 读了很多 JavaScript 经典书籍、红宝书、犀牛书(看了 60%)、ES6、高性能 js、你不知道的 js 系列、忍者秘籍等等,并且感悟深的都有在各个平台上留下相关笔记。

在这个阶段,你有太多需要学习的了,任何你不知道的,你都应该知道!不要等工作、业务上来给你知识盲区扫描。自己主动找自己的技术方向。有目的、有结果性的学习~

掌握阶段

其实就我个人感觉,我应该属于第二阶段往第三阶段过渡的一个阶段,所以这里不能给出我个人的总结了。说下这个阶段,我自己的个人规划供大家参考吧。

这个阶段的我,已经进入到了自己心仪的公司。并且身边的大牛几乎是每天都能给到自己压力。所以学习。。。依旧是我最为核心的目标。但是同时!业务的理解和掌握,也是我这个阶段要去提升和重视的一点。

这个阶段,我需要做的很多。说一下对自己的期望

工作上

  • 带有业务思考的去编写每一行代码。对于代码规范、组件的封装、整体架构的搭建需要进一步的去思考、学习。
  • 明白Bu的核心利益是什么,你对Bu 的贡献点、以及如何利用好自己的技术来反哺业务。
  • 多从业务上去寻找技术的突破点。从技术的突破点去寻找自己的方向。
  • 从前端团队的角度去思考如何解放前端脑动力。时刻保持敏锐的嗅觉去思考团队的开发流程、技术痛点等,并努力寻求解决办法。

学习上

  • 学习基于业务。但是依旧要明确自己的未来领域。
  • 多做技术分享,多和大牛接触、以提高自己的技术视野和未来前端方向的嗅觉
  • 再重温一次前端,多些总结性文章。
  • 对于前端领域现有知识,不要求能够面面俱到、但是能够做到提纲挈领
  • 保持一颗有空就学习的心
  • 提高自己非技术以外的软实力(作图、架构思考、做 PPT 等)
  • 个人品牌影响力的打造(不得不说,github 帮我敲开的阿里大门)

总结

总结如上所说,其实我没有走任何捷径。只不过

  • 学习东西果断、坚持。并且一定会有产出(博客、github)
  • 不怕遇到问题,甚至主动找别人遇到的的问题,然后自己帮忙解答(技术交流群)
  • 学习新东西只是浏览一遍官网介绍和 api,然后直接上手写 demo、不会再去查!
  • 多浏览技术论坛、博客。常备梯子你懂得。多和大牛接触,交流(但是注意:没有大牛是闲着的)
  • 一定要写!写!写!不要只会看!读!
  • 技术不能脱离业务,多去思考业务痛点、团队工作流痛点、技术突破点。
  • 提高自己的技术思考能力,不仅仅要学习,更要学会去创新、去思考 why。

最后,我想说,其实我依旧还有很多需要学习的地方。此篇文章,是对一直以来支持我的哥们一些疑惑的解答。因为我的确给不了最为有效的学习方法和建议。所以只能简述自己的情况。如若说的不对的地方,还望见谅。

勿忘初心!狂而不傲 peace~

福利

微信公众号内回复:【简历】 获取笔者撬开大厂大门的简历

下一篇我将介绍:阿里一面,我是如何面试 p6、p7 的(面试题以及打分分析)

flutter从入门到能寄几玩儿

国庆后面两天在家学习整理了一波flutter,基本把能撸过能看到的代码都过了一遍,此文篇幅较长,建议保存(star)再看。传送门: Nealyang personal blog

前言

毕竟前端出生,找(qi)到(shi)了(bing)感(mei)觉(ru)后(men),其实就是一个UI框架,只不过他的引擎基于C++,底层基于Skia渲染,DartVM虚拟机以及Text and so on...

2018年6月21日Google发布Flutter首个release预览版,作为Google baba大力推出的一种全新的响应式,跨平台,高性能的移动开发框架,势必会火一波~没别的,就是因为Google baba,当然,从目前看来也的确越来越火了。
Questions tagged [flutter]
img

本文我们从介绍flutter基本概念到梳理常用Widget到常用app demos编写到放弃,希望可以帮助每一个像我一样的初学者。有误地方还望大神不吝赐教~

img

国际惯例,吹一波先~

直接移步Flutter官宣ppt

关于Dart

作为Flutter入门文章,Dart必然少不了,当然,作为Flutter入门篇,Dart预发基础必然不会过多介绍。

Dart入门传送门:Dart or Dart2,或者从Dart中文网中学习也不错其实.

这里我们说说为啥是Dart。

许多语言科学家认为,一个人说的自然语言会影响他们的思维方式。早起Flutter团队评估了十多种语言最终选择了Dart,因为它符合他们构建用户界面的方式

  • Dart 是AOT 编译的,编译成快速可预测的本地代码,使Flutter几乎都可以使用Dart编写,这不仅使Flutter变的更快,而且几乎所有的东西都可以定制
  • Dart也可以JIT编译,开发周期异常快,工作流颠覆常规,也使得Flutter可以实现非常Diao的有状态热重载(别扯别的,人家是出生自带哇)
  • Dart可以更轻松地创建以60fps运行的流畅动画和转场。Dart可以在没有锁的情况下进行对象分配和垃圾回收。就像JavaScript一样,Dart避免了抢占式调度和共享内存(因而也不需要锁)。由于Flutter应用程序被编译为本地代码,因此它们不需要在领域之间建立缓慢的桥梁(例如,JavaScript到本地代码)。它的启动速度也快得多
  • Dart使Flutter不需要单独的声明式布局语言,如JSX或XML,或单独的可视化界面构建器,因为Dart的声明式编程布局易于阅读和可视化。所有的布局使用一种语言,聚集在一处,Flutter很容易提供高级工具,使布局更简单
  • Dart对于IOS、Android、Web FE来说,都还比较友好。

具体选择Dart的原因,以及向了解Dart的,移步为什么Flutter会选择 Dart

关于Flutter

刚开始接触flutter心中难免会有疑惑,不是已经有RN、Weex等各种跨平台移动开发 了,flutter优势在哪呢? 看我从网上盗的图!

img

图片来源:简书 作者:AWeiLoveAndroidAWeiLoveAndroid

Everything is Widget

有一种说法认为函数式语言和命令式语言的不同在于命令式语言是给计算机下达指令而函数式语言是向计算机描述逻辑。这种思路在Flutter UI中得到了体现。Flutter不提倡去操作UI,它当然也基本不会提供操作View的API,比如我们常见的类似TextView.setText(),Button.setOnClick()这种是不会有的。对界面的描述是可以数据化的(类似XML,JSON等),而对界面的操作是很难数据化的,这很重要,响应式需要方便可持续的将数据映射成界面。

在Flutter中用Widget来描述界面,Widget只是View的“配置信息”,编写的时候利用Dart语言一些声明式特性来得到类似结构化标记语言的可读性。Widget根据布局形成一个层次结构。每个widget嵌入其中,并继承其父项的属性。没有单独的“应用程序”对象,相反,根widget扮演着这个角色。在Flutter中,一切皆为Widget,甚至包括css样式。

<div class="greybox">
    Lorem ipsum
</div>

.greybox {
      background-color: #e0e0e0; /* grey 300 */
      width: 320px;
      height: 240px;
      font: 900 24px Georgia;
    }

在flutter中我们编写为

var container = new Container( // grey box
  child: new Text(
    "Lorem ipsum",
    style: new TextStyle(
      fontSize: 24.0
      fontWeight: FontWeight.w900,
      fontFamily: "Georgia",
    ),
  ),
  width: 320.0,
  height: 240.0,
  color: Colors.grey[300],
);

可以看到我们css样式中的font定义的样式,在flutter中,需要new TextStyleTextStyle就是一个Widget,并且样式必须作用与Container中的child:text上,不存在web中样式的继承。

刚开始接触的同学就类比于react中扯的,一切皆为组件吧,其实widget是对页面UI的一种描述。他功能类有点似于android中的xml,react中的jsx。widget在渲染的时候会转化成element。Element相比于widget增加了上下文的信息。element是对应widget,在渲染树的实例化节点。由于widget是immutable的,所以同一个widget可以同时描述多个渲染树中的节点。但是Element是描述固定在渲染书中的某一个特定位置的点。简单点说widget作为一种描述是可以复用的,但是element却跟需要绘制的节点一一对应。那element是最终渲染的view么?抱歉,还不是。element绘制时会转化成rendObject。RendObject才是真正经过layout和paint并绘制在屏幕上的对象。在flutter中有三套渲染相关的tree,分别是:widget tree, element tree & rendObject tree。三者的渲染流程如下:

img

有没有一种 jsx -> virtual Dom -> real dom滴感觉呢~
img

咳咳,后面会介绍基础常用的Widget配合一些demo,大家可能对这个体会就会更加清晰一些。

组合大于继承

Flutter中很多借鉴了react的**,甚至包括后面会说到的state。

Widget本身通常由许多更小的、单一的小小widget组成,甚至小到它单一下来并没有什么作用的感觉,这些Widget几几组合形成一个强大的自定义的大大Widget。

比如一个Container,对于Web FE来说可能就是个div,而他就是由很多的widget组成,这些widget负责布局、绘制、定位、大小等。我们可以使用各种姿势来组合他们而不是继承他们。类层次结构很浅且很宽,可以最大限度的增加可能组合的数量

img

框架结构

img
上面的图片是Flutter分层框架结构图,对大部分开发者而言,最常用的是Widgets层,屏幕上可见与不可见的元素都由Widgets层实现,这些元素被称为Widget。在Widgets层在上层,有两个现成的Widget库,Material库即Material Design的Widget库,Material Design是Google I/O 2014发布的设计语言,目前成为统一Android Mobile、Android Table、Desktop Chrome等平台的设计语言规范。Cupertino库则是一个模仿iOS设计风格的Widget库。

底层是Flutter Engine虚拟机,在这一层次中需要了解一下的是Skia,Skia是Google研发的包括图形、文本、图像、动画等多方面的图形引擎,不仅用于Google Chrome浏览器,Android系统也采用Skia作为绘图处理引擎。

GPU渲染:
img

state生命周期:
img

作为初学者看上面的图有点云里雾里的,且先做到心里有数~

Flutter走马观花

关于Flutter环境问题这里不再赘述
此后~大量代码来袭

基础Widget之material版Hello world

国际惯例,hello world

import 'package:flutter/material.dart';

class MyAppBar extends StatelessWidget{
  MyAppBar({this.title});//
  final Widget title;

  @override
  Widget build(BuildContext context){
    return new Container(
      height: 56.0,
      padding: const EdgeInsets.symmetric(horizontal:8.0),
      decoration: new BoxDecoration(
        color:Colors.blue[400]
      ),
      child: Row(
        children: <Widget>[
          new IconButton(
            icon:new Icon(Icons.menu),
            tooltip:'Navigation menu',
            onPressed: (){
              print('点击Menu');
            },
          ),
          new Expanded(
            child:new Center(
              child:title
            )
          ),
          new IconButton(
            icon:Icon(Icons.search),
            tooltip:'Search',
            onPressed: (){
              print('点击搜索按钮');
            },
          )
        ],
      ),
    );
  }
}

class MyScaffold extends StatelessWidget{
  @override 
  Widget build(BuildContext context){
    return Material(
      child: new Column(
        children:<Widget>[
          new MyAppBar(
            title:new Text(
              'Hello World',
              style:Theme.of(context).primaryTextTheme.title
             ),
          ),
          new Expanded(
            child:new Center(
              child:Text('Hello World!!!')
            )
          )
        ]
      ),
    );
  }
}

void main(){
  runApp(
    new MaterialApp(
      title:'My app',
      home:new MyScaffold()
    )
  );
}

img
代码地址:https://github.com/Nealyang/flutter

这个UI的确有些对不起人了,上面的title被挡住了。且先不去适配,后面我们使用Material提供的Scaffold即可

第一个例子,重点说下代码(用过的Widget记住):

  • 一切都是Widget,且Widget前面的new可有可无。
  • 类MyAppBar和MyScaffold中使用了Container、Row、Column、Text、IconButton、Icon、BoxDecoration、Center、Expanded等常用Widget
    • Container一个拥有绘制、定位、调整大小的 widget。类似于div,我们可以用它来创建矩形视图,container 可以装饰为一个BoxDecoration, 如 background、一个边框、或者一个阴影。 Container 也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。另外, Container可以使用矩阵在三维空间中对其进行变换。
    • RowColumn其实就是flex布局中的flex-direction
    • Expanded它会填充尚未被其他子项占用的的剩余可用空间。Expanded可以拥有多个children。然后使用flex参数来确定他们占用剩余空间的比例。更多细节可以参看:flutter控件Flexible和 Expanded的区别
  • 先定义了一个MyAppBar的类,构造函数中接受一个Widget的title,其实我们也可以接受String title然后在类中自己去new Title(title)
  • runApp函数接受给定的Widget并使用其作为widget根。
  • widget的主要工作是实现一个build函数,用以构建自身。一个widget通常由一些较低级别widget组成。Flutter框架将依次构建这些widget,直到构建到最底层的子widget时,这些最低层的widget通常为RenderObject,它会计算并描述widget的几何形状。

基本交互之material版Hello world

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // app的根Widget
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        // 这是设置的app主题
        // 运行后你可以看到app有一个蓝色的toobar,并且在不退出app的情况下修改代码会热更新
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

// 这是应用中一个基类,继承自StateFulWidget,意味着这个类拥有一个state对象,该对象里的一些字段会影响app的UI
// 这个类是state的一些配置项。通过构造函数来获取值,这个值一般在State中消费,并且使用final关键字。其实类似于react中的defaultProps

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      // setState方法告诉Flutter,这个State中有些值发生了变化,以便及时将新值更新到UI上,
      // 如果我不通过setState更改_count字段,那么Flutter并不会调用build匿名函数去更新界面
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // build方法会在每次setState的时候重新运行,例如上面的_incrementCounter方法被调用
    //Flutter已经被优化了重新构建的方法,所以你只会去更新需要去更新的部分,不必去单独更新里面的一些更细小的widget,类似于React中diff
    return new Scaffold(
      appBar: new AppBar(
        // 这里我们使用从App.build方法中初始化MyHomePage时候传入的title值来设置我们的title
        title: new Text(widget.title),
      ),
      body: new Center(
        // Center是一个布局Widget,他只有一个child(区分row or cloumn等是children),并且会将child的widget居中显示
        child: new Column(
          // Column也是一个布局widget,他可以有多个子widget
          // Column 有很多的属性去控制他的大小以及子widget的位置,这里我们使用mainAxisAlignment来让children在垂直线上居中,
          // 这里的主轴就是垂直的,因为Column就是垂直方向的,这里可以大概想象为display:flex,flex-directions:column,align-item,justifyContent。。。
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'Hello World!',
              style:TextStyle(
                fontSize:24.0,
                color: Colors.redAccent,
                decorationStyle:TextDecorationStyle.dotted,
                fontWeight: FontWeight.bold,
                fontStyle: FontStyle.italic,
                decoration: TextDecoration.underline
              )
            ),
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ),//最后这个逗号有利于格式化代码
    );
  }
}

img
注释上基本已经加了,这里重点说下,StatefulWidget和StatelessWidget.

  • Stateless widgets 是不可变的,这意味着它们的属性不能改变——所有的值都是 final
  • Stateful widgets 持有的状态可能在 widget 生命周期中发生变化,实现一个 stateful widget 至少需要两个类:1)一个 StatefulWidget 类;2)一个 State 类,StatefulWidget 类本身是不变的,但是 State 类在 widget 生命周期中始终存在
  • 如果需要变化需要重新创建。StatefulWidget可以保存自己的状态。那问题是既然widget都是immutable的,怎么保存状态?其实Flutter是通过引入了State来保存状态。当State的状态改变时,能重新构建本节点以及孩子的Widget树来进行UI变化。注意:如果需要主动改变State的状态,需要通过setState()方法进行触发,单纯改变数据是不会引发UI改变的。

还有关于key的部分这里就不做介绍了,其实就类似与react中key的概念,便于diff,提高效率的。
具体可以查看 Key

到这里,我们看到了Flutter的一些基本用法,Widget的套用、样式的编写、事件的注册,如果再学习下一些路由、请求、缓存是不是就可以自己开发APP了呢img

OK,强化下编写界面,咱再来些demo吧~

布局Widget

img

自己写的后,发现跟官网实现方式不同,代码地址

具体实现可以参照官网教程

这里不再赘述,下面我们说下对于布局的理解和感受以及常用布局widget。

从一个前端的角度来说,说到画界面,可能还是对布局这块比较敏感

img

![img](![IMAGE](quiver-image-url/F4BD928A46B63086BE446CC582FB40D4.jpg =878x367))

当然,这里我们还是说下目前常用的flex布局,基本拿到页面从大到小拆分后就是如上图。

所以Widget布局其实也就是Row和Column用的最多,然后由于Flutter一切皆为组件的理念,可能会需要用到别的类css布局的Widget,譬如:Container。其实咱就理解为块元素吧!

下面简单演示下一些常用的Widget,这里就不在赘述Row和Column了。传送门:布局Widget

Container

可以添加padding、margin、border、background color、通常用于装饰其他Widget

img

代码链接 Nealyang/flutter

class MyHomePage extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    Container cell (String imgSrc){
      return new Container(
        decoration: new BoxDecoration(
          border:Border.all(width:6.0,color:Colors.black38),
          borderRadius: BorderRadius.all(const Radius.circular(8.0))
        ),
        child: Image.asset(
          'images/$imgSrc',
          width: 180.0,
          height: 180.0,
          fit: BoxFit.cover,
        ),
      );
    }

    return Container(
      padding: const EdgeInsets.all(10.0),
      color: Colors.grey,
      child: new Column(
        mainAxisSize: MainAxisSize.min,
        children:<Widget>[
          new Container(
            margin: const EdgeInsets.only(bottom:10.0),
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children:<Widget>[
                cell('1.jpg'),
                cell('2.jpg')
              ]
            ),
          ),
          new Container(
            margin: const EdgeInsets.only(bottom:10.0),
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children:<Widget>[
                cell('3.jpg'),
                cell('4.jpg')
              ]
            ),
          ),
        ]
      ),
    );
  }
}

该布局中每个图像使用一个Container来添加一个圆形的灰色边框和边距。然后使用容器将列背景颜色更改为浅灰色。

GridView

可滚动的网格布局,理解为display:grid

GridView提供两个预制list,当GridView检测到内容太长时,会自动滚动。如果需要构建自定义grid,可是使用GridView.countGridView.extent来指定允许设置的列数以及指定项最大像素宽度。

img

代码链接 Nealyang/flutter

List<Container> _buildGridTileList(int count) {

  return new List<Container>.generate(
      count,
      (int index) =>
          new Container(child: new Image.asset('images/${index+1}.jpg')));
}

Widget buildGrid() {
  return new GridView.extent(
      maxCrossAxisExtent: 150.0,
      padding: const EdgeInsets.all(4.0),
      mainAxisSpacing: 4.0,
      crossAxisSpacing: 4.0,
      children: _buildGridTileList(10));
}

class MyHomePage extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return  new Center(
        child: buildGrid(),
      );
  }
}

如上是指定maxCrossAxisExtent,我们可以直接去指定列数,例如官网的代码实例:

new GridView.count(
  primary: false,
  padding: const EdgeInsets.all(20.0),
  crossAxisSpacing: 10.0,
  crossAxisCount: 3,
  children: <Widget>[
    const Text('He\'d have you all unravel at the'),
    const Text('Heed not the rabble'),
    const Text('Sound of screams but the'),
    const Text('Who scream'),
    const Text('Revolution is coming...'),
    const Text('Revolution, they...'),
  ],
)

通过crossAxisCount直接指定列数。

Stack

层叠布局,position为absolute的感jio~

使用Stack来组织需要重叠的widget。widget可以完全或部分重叠底部widget。子列表中的第一个widget是base widget; 随后的子widget被覆盖在基础widget的顶部。Stack的内容不能滚动。有点类似于weex中的设置了absolute的感觉。底部组件永远在上面组件的上面。

ListView

可滚动的长列表,可以水平或者垂直。

Card

Material风格组件,卡片,AntD啥的组件库经常会出现的那种组件。

在flutter中,Card具有圆角和阴影,更改Card的elevation属性可以控制阴影效果。

ListTile

Material风格组件,我理解为常用的列表Item的样式,最多三行文字,可选的行前、行尾的图标

img

代码链接 Nealyang/flutter

总结

从目前我个人浅薄的Flutter技能来说,最大的困难可能是找不到合适的Widget去实现想要的布局或者效果,甚至包括css样式作用于那个Widget,譬如Opacity是一个widget而不是一个css样式~

所以对于Flutter,我们还是要多折腾,多些demo,类似网上很多仿xxxApp等~

对于画界面,更多的还可以参看下官网教程:Flutter for Web开发者

一切才刚刚开始

Flutter一切基于Widget,搞定widget就好比,搞定英语单词一样,单词、词组都贼6了还怕英语?

别急别急,借用张晟哥的图来给大家消消火气~

widgets

所以说,Flutter有一个庞大的组件体系,需要花费非常多的时间去梳理。

!更重要的是:多实践

本来最后一章是自己写的一个demo的讲解~

可惜时间评估不准确,漏评估了假期惰性。。。考虑篇幅,后面补上仿XXX的Demo吧~~

img

参考链接 && 好文推荐

Demo 推荐

create-react-app 源码学习(上)

前言

对于前端工程构建,很多公司、BU 都有自己的一套构建体系,比如我们正在使用的 def,或者 vue-cli 或者 create-react-app,由于笔者最近一直想搭建一个个人网站,秉持着呼吸不停,折腾不止的原则,编码的过程中,还是不想太过于枯燥。在 coding 之前,搭建自己的项目架构的时候,突然想,为什么之前搭建过很多的项目架构不能直接拿来用,却还是要从 0 到 1 的去写 webpack 去下载相关配置呢?遂!学习下 create-react-app 源码,然后自己搞一套吧~

create-react-app 源码

代码的入口在 packages/create-react-app/index.js下,核心代码在createReactApp.js中,虽然有大概 900+行代码,但是删除注释和一些友好提示啥的大概核心代码也就六百多行吧,我们直接来看

index.js

img

index.js 的代码非常的简单,其实就是对 node 的版本做了一下校验,如果版本号低于 8,就退出应用程序,否则直接进入到核心文件中,createReactApp.js

createReactApp.js

createReactApp 的功能也非常简单其实,大概流程:

  • 命令初始化,比如自定义create-react-app --info 的输出等
  • 判断是否输入项目名称,如果有,则根据参数去跑安装,如果没有,给提示,然后退出程序
  • 修改 package.json
  • 拷贝 react-script 下的模板文件

准备工作:配置 vscode 的 debug 文件

        {
            "type": "node",
            "request": "launch",
            "name": "CreateReactApp",
            "program": "${workspaceFolder}/packages/create-react-app/index.js",
            "args": [
                "study-create-react-app-source"
            ]
        },
        {
            "type": "node",
            "request": "launch",
            "name": "CreateReactAppNoArgs",
            "program": "${workspaceFolder}/packages/create-react-app/index.js"
        },
        {
            "type": "node",
            "request": "launch",
            "name": "CreateReactAppTs",
            "program": "${workspaceFolder}/packages/create-react-app/index.js",
            "args": [
                "study-create-react-app-source-ts --typescript"
            ]
        }

这里我们添加三种环境,其实就是 create-react-app 的不同种使用方式

  • create-react-app study-create-react-app-source
  • create-react-app
  • create-react-app study-create-react-app-source-ts --typescript

commander 命令行处理程序

commander 文档传送门


let projectName;

const program = new commander.Command(packageJson.name)
  .version(packageJson.version)//create-react-app -v 时候输出的值 packageJson 来自上面 const packageJson = require('./package.json');
  .arguments('<project-directory>') //定义 project-directory ,必填项
  .usage(`${chalk.green('<project-directory>')} [options]`)
  .action(name => {
    projectName = name;//获取用户的输入,存为 projectName
  })
  .option('--verbose', 'print additional logs')
  .option('--info', 'print environment debug info')
  .option(
    '--scripts-version <alternative-package>',
    'use a non-standard version of react-scripts'
  )
  .option('--use-npm')
  .option('--use-pnp')
  .option('--typescript')
  .allowUnknownOption()
  .on('--help', () => {// on('option', cb) 语法,输入 create-react-app --help 自动执行后面的操作输出帮助
    console.log(`    Only ${chalk.green('<project-directory>')} is required.`);
    console.log();
    console.log(
      `    A custom ${chalk.cyan('--scripts-version')} can be one of:`
    );
    console.log(`      - a specific npm version: ${chalk.green('0.8.2')}`);
    console.log(`      - a specific npm tag: ${chalk.green('@next')}`);
    console.log(
      `      - a custom fork published on npm: ${chalk.green(
        'my-react-scripts'
      )}`
    );
    console.log(
      `      - a local path relative to the current working directory: ${chalk.green(
        'file:../my-react-scripts'
      )}`
    );
    console.log(
      `      - a .tgz archive: ${chalk.green(
        'https://mysite.com/my-react-scripts-0.8.2.tgz'
      )}`
    );
    console.log(
      `      - a .tar.gz archive: ${chalk.green(
        'https://mysite.com/my-react-scripts-0.8.2.tar.gz'
      )}`
    );
    console.log(
      `    It is not needed unless you specifically want to use a fork.`
    );
    console.log();
    console.log(
      `    If you have any problems, do not hesitate to file an issue:`
    );
    console.log(
      `      ${chalk.cyan(
        'https://github.com/facebook/create-react-app/issues/new'
      )}`
    );
    console.log();
  })
  .parse(process.argv);

关于 commander 的使用,这里就不介绍了,对于 create-react-app 的流程我们需要知道的是,它,初始化了一些 create-react-app 的命令行环境,这一波操作后,我们可以看到 program 张这个样纸:

img

接着往下走

img

当我们 debug 启动 noArgs 环境的时候,走到这里就结束了,判断 projectName 是否为 undefined,然后输出相关提示信息,退出~

createApp

在查看 createApp function 之前,我们再回头看下命令行的一些参数定义,方便我们理解 createApp 的一些参数

我们使用

        {
            "type": "node",
            "request": "launch",
            "name": "CreateReactAppTs",
            "program": "${workspaceFolder}/packages/create-react-app/index.js",
            "args": [
                "study-create-react-app-source-ts",
                "--typescript",
                "--use-npm"
            ]
        }

debugger 我们项目的时候,就可以看到,program.typescripttrueuseNpmtrue,当然,这些也都是我们在commander中定义的 options,所以源码里面 createApp 中,我们传入的参数分别为:

  • projectName : 项目名称
  • program.verbose 是否输出额外信息
  • program.scriptsVersion 传入的脚本版本
  • program.useNpm 是否使用 npm
  • program.usePnp 是否使用 Pnp
  • program.typescript 是否使用 ts
  • hiddenProgram.internalTestingTemplate 给开发者用的调试模板路径
function createApp(
  name,
  verbose,
  version,
  useNpm,
  usePnp,
  useTypescript,
  template
) {
  const root = path.resolve(name);//path 拼接路径
  const appName = path.basename(root);//获取文件名

  checkAppName(appName);//检查传入的文件名合法性
  fs.ensureDirSync(name);//确保目录存在,如果不存在则创建一个
  if (!isSafeToCreateProjectIn(root, name)) { //判断新建这个文件夹是否安全,否则直接退出
    process.exit(1);
  }

  console.log(`Creating a new React app in ${chalk.green(root)}.`);
  console.log();

  const packageJson = {
    name: appName,
    version: '0.1.0',
    private: true,
  };
  fs.writeFileSync(
    path.join(root, 'package.json'),
    JSON.stringify(packageJson, null, 2) + os.EOL
  );//写入 package.json 文件

  const useYarn = useNpm ? false : shouldUseYarn();//判断是使用 yarn 呢还是 npm
  const originalDirectory = process.cwd();
  process.chdir(root);
  if (!useYarn && !checkThatNpmCanReadCwd()) {//如果是使用npm,检测npm是否在正确目录下执行
    process.exit(1);
  }

  if (!semver.satisfies(process.version, '>=8.10.0')) {//判断node环境,输出一些提示信息, 并采用旧版本的 react-scripts
    console.log(
      chalk.yellow(
        `You are using Node ${
          process.version
        } so the project will be bootstrapped with an old unsupported version of tools.\n\n` +
          `Please update to Node 8.10 or higher for a better, fully supported experience.\n`
      )
    );
    // Fall back to latest supported react-scripts on Node 4
    version = '[email protected]';
  }

  if (!useYarn) {//关于 npm、pnp、yarn 的使用判断,版本校验等
    const npmInfo = checkNpmVersion();
    if (!npmInfo.hasMinNpm) {
      if (npmInfo.npmVersion) {
        console.log(
          chalk.yellow(
            `You are using npm ${
              npmInfo.npmVersion
            } so the project will be bootstrapped with an old unsupported version of tools.\n\n` +
              `Please update to npm 5 or higher for a better, fully supported experience.\n`
          )
        );
      }
      // Fall back to latest supported react-scripts for npm 3
      version = '[email protected]';
    }
  } else if (usePnp) {
    const yarnInfo = checkYarnVersion();
    if (!yarnInfo.hasMinYarnPnp) {
      if (yarnInfo.yarnVersion) {
        console.log(
          chalk.yellow(
            `You are using Yarn ${
              yarnInfo.yarnVersion
            } together with the --use-pnp flag, but Plug'n'Play is only supported starting from the 1.12 release.\n\n` +
              `Please update to Yarn 1.12 or higher for a better, fully supported experience.\n`
          )
        );
      }
      // 1.11 had an issue with webpack-dev-middleware, so better not use PnP with it (never reached stable, but still)
      usePnp = false;
    }
  }

  if (useYarn) {
    let yarnUsesDefaultRegistry = true;
    try {
      yarnUsesDefaultRegistry =
        execSync('yarnpkg config get registry')
          .toString()
          .trim() === 'https://registry.yarnpkg.com';
    } catch (e) {
      // ignore
    }
    if (yarnUsesDefaultRegistry) {
      fs.copySync(
        require.resolve('./yarn.lock.cached'),
        path.join(root, 'yarn.lock')
      );
    }
  }

  run(
    root,
    appName,
    version,
    verbose,
    originalDirectory,
    template,
    useYarn,
    usePnp,
    useTypescript
  );
} 

代码非常简单,部分注释已经加载代码中,简单的说就是对一个本地环境的一些校验,版本检查啊、目录创建啊啥的,如果创建失败,则退出,如果版本较低,则使用对应低版本的create-react-app,最后调用 run 方法

checkAppName

这些工具方法,其实在写我们自己的构建工具的时候,也可以直接 copy 的哈,所以这里我们也是简单看下里面的实现,

checkAPPName 方法主要的核心代码是validate-npm-package-name package,从名字即可看出,检查是否为合法的 npm 包名

var done = function (warnings, errors) {
  var result = {
    validForNewPackages: errors.length === 0 && warnings.length === 0,
    validForOldPackages: errors.length === 0,
    warnings: warnings,
    errors: errors
  }
  if (!result.warnings.length) delete result.warnings
  if (!result.errors.length) delete result.errors
  return result
}
...
...
var validate = module.exports = function (name) {
  var warnings = []
  var errors = []

  if (name === null) {
    errors.push('name 不能使 null')
    return done(warnings, errors)
  }

  if (name === undefined) {
    errors.push('name 不能是 undefined')
    return done(warnings, errors)
  }

  if (typeof name !== 'string') {
    errors.push('name 必须是 string 类型')
    return done(warnings, errors)
  }

  if (!name.length) {
    errors.push('name 的长度必须大于 0')
  }

  if (name.match(/^\./)) {
    errors.push('name 不能以点开头')
  }

  if (name.match(/^_/)) {
    errors.push('name 不能以下划线开头')
  }

  if (name.trim() !== name) {
    errors.push('name 不能包含前空格和尾空格')
  }

  // No funny business
  // var blacklist = [
  //   'node_modules',
  //   'favicon.ico'
  // ]
  blacklist.forEach(function (blacklistedName) {
    if (name.toLowerCase() === blacklistedName) { //不能是“黑名单”内的
      errors.push(blacklistedName + ' is a blacklisted name')
    }
  })

  // Generate warnings for stuff that used to be allowed
  // 为以前允许的内容生成警告

 // 后面的就不再赘述了

  return done(warnings, errors)
}

img

最终,checkAPPName返回的东西如截图所示,后面写代码可以直接拿来借鉴!借鉴~

isSafeToCreateProjectIn

所谓安全性校验,其实就是检查当前目录下是否存在已有文件。

checkNpmVersion

后面的代码也都比较简单,这里就不展开说了,版本比较实用的是一个semver package.

run

代码跑到这里,该检查的都检查了,鸡也不叫了、狗也不咬了,该干点正事了~

run 主要做的事情就是安装依赖、拷贝模板。

getInstallPackage做的事情非常简单,根据传入的 version 和原始路径 originalDirectory 去获取要安装的 package 列表,默认情况下version 为 undefined,获取到的 packageToInstall 为react-scripts,也就是我们如上图的 resolve 回调。

最终,我们拿到需要安装的 info 为

{
  isOnline:true,
  packageName:"react-scripts"
}

当我们梳理好需要安装的 package 后,就交给 npm 或者 yarn 去安装我们的依赖即可

spawn执行完命令后会有一个回调,判断code是否为 0,然后 resolve Promise,

 .then(async packageName => {
         // 安装完 react, react-dom, react-scripts 之后检查当前环境运行的node版本是否符合要求
        checkNodeVersion(packageName);
        // 检查 package.json 中的版本号
        setCaretRangeForRuntimeDeps(packageName);

        const pnpPath = path.resolve(process.cwd(), '.pnp.js');

        const nodeArgs = fs.existsSync(pnpPath) ? ['--require', pnpPath] : [];

        await executeNodeScript(
          {
            cwd: process.cwd(),
            args: nodeArgs,
          },
          [root, appName, verbose, originalDirectory, template],
          `
        var init = require('${packageName}/scripts/init.js');
        init.apply(null, JSON.parse(process.argv[1]));
      `
        );

create-react-app之前的版本中,这里是通过调用react-script下的 init方法来执行后续动作的。这里通过调用executeNodeScript 方法

function executeNodeScript({ cwd, args }, data, source) {
  // cwd:"/Users/nealyang/Desktop/create-react-app/study-create-react-app-source"

  // data:
  // 0:"/Users/nealyang/Desktop/create-react-app/study-create-react-app-source"
  // 1:"study-create-react-app-source"
  // 2:undefined
  // 3:"/Users/nealyang/Desktop/create-react-app"
  // 4:undefined

  // source
  // "  var init = require('react-scripts/scripts/init.js');
  //   init.apply(null, JSON.parse(process.argv[1]));
  // "
  
  return new Promise((resolve, reject) => {
    const child = spawn(
      process.execPath,
      [...args, '-e', source, '--', JSON.stringify(data)],
      { cwd, stdio: 'inherit' }
    );

    child.on('close', code => {
      if (code !== 0) {
        reject({
          command: `node ${args.join(' ')}`,
        });
        return;
      }
      resolve();
    });
  });
}

executeNodeScript 方法主要是通过 spawn 来通过 node命令执行react-script下的 init 方法。所以截止当前,create-react-app完成了他的工作:npm i ,

react-script/init.js

修改 vscode 的 debugger 配置,然后我们来 debugger react-script 下的 init 方法

function init(appPath, appName, verbose, originalDirectory, template) {
  // 获取当前包中包含 package.json 所在的文件夹路径
  const ownPath = path.dirname(
    //"/Users/nealyang/Desktop/create-react-app/packages/react-scripts"
    require.resolve(path.join(__dirname, '..', 'package.json'))
  );
  const appPackage = require(path.join(appPath, 'package.json')); //项目目录下的 package.json
  const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock')); //通过判断目录下是否有 yarn.lock 来判断是否使用 yarn

  // Copy over some of the devDependencies
  appPackage.dependencies = appPackage.dependencies || {};

  //   react:"16.8.6"
  // react-dom:"16.8.6"
  // react-scripts:"3.0.1"
  const useTypeScript = appPackage.dependencies['typescript'] != null;

  // Setup the script rules 设置 script 命令
  appPackage.scripts = {
    start: 'react-scripts start',
    build: 'react-scripts build',
    test: 'react-scripts test',
    eject: 'react-scripts eject',
  };

  // Setup the eslint config 这是 eslint 的配置
  appPackage.eslintConfig = {
    extends: 'react-app',
  };

  // Setup the browsers list 组件autoprefixer、bable-preset-env、eslint-plugin-compat、postcss-normalize共享使用的配置项 (感谢网友指正)
  appPackage.browserslist = defaultBrowsers;

  // 写入我们需要创建的目录下的 package.json 中
  fs.writeFileSync(
    path.join(appPath, 'package.json'),
    JSON.stringify(appPackage, null, 2) + os.EOL
  );

  const readmeExists = fs.existsSync(path.join(appPath, 'README.md'));
  if (readmeExists) {
    fs.renameSync(
      path.join(appPath, 'README.md'),
      path.join(appPath, 'README.old.md')
    );
  }

  // Copy the files for the user  获取模板的路径
  const templatePath = template //"/Users/nealyang/Desktop/create-react-app/packages/react-scripts/template"
    ? path.resolve(originalDirectory, template)
    : path.join(ownPath, useTypeScript ? 'template-typescript' : 'template');
  if (fs.existsSync(templatePath)) {
    // 这一步就过分了, 直接 copy!  appPath:"/Users/nealyang/Desktop/create-react-app/study-create-react-app-source"
    fs.copySync(templatePath, appPath);
  } else {
    console.error(
      `Could not locate supplied template: ${chalk.green(templatePath)}`
    );
    return;
  }

  // Rename gitignore after the fact to prevent npm from renaming it to .npmignore 重命名gitignore以防止npm将其重命名为.npmignore
  // See: https://github.com/npm/npm/issues/1862
  try {
    fs.moveSync(
      path.join(appPath, 'gitignore'),
      path.join(appPath, '.gitignore'),
      []
    );
  } catch (err) {
    // Append if there's already a `.gitignore` file there
    if (err.code === 'EEXIST') {
      const data = fs.readFileSync(path.join(appPath, 'gitignore'));
      fs.appendFileSync(path.join(appPath, '.gitignore'), data);
      fs.unlinkSync(path.join(appPath, 'gitignore'));
    } else {
      throw err;
    }
  }

  let command;
  let args;

  if (useYarn) {
    command = 'yarnpkg';
    args = ['add'];
  } else {
    command = 'npm';
    args = ['install', '--save', verbose && '--verbose'].filter(e => e);
  }
  args.push('react', 'react-dom');
  // args Array
  // 0:"install"
  // 1:"--save"
  // 2:"react"
  // 3:"react-dom"

  // 安装其他模板依赖项(如果存在)
  const templateDependenciesPath = path.join(//"/Users/nealyang/Desktop/create-react-app/study-create-react-app-source/.template.dependencies.json"
    appPath,
    '.template.dependencies.json'
  );
  if (fs.existsSync(templateDependenciesPath)) {
    const templateDependencies = require(templateDependenciesPath).dependencies;
    args = args.concat(
      Object.keys(templateDependencies).map(key => {
        return `${key}@${templateDependencies[key]}`;
      })
    );
    fs.unlinkSync(templateDependenciesPath);
  }

  // 安装react和react-dom以便与旧CRA cli向后兼容
  // 没有安装react和react-dom以及react-scripts
  // 或模板是presetend(通过--internal-testing-template)
  if (!isReactInstalled(appPackage) || template) {
    console.log(`Installing react and react-dom using ${command}...`);
    console.log();

    const proc = spawn.sync(command, args, { stdio: 'inherit' });
    if (proc.status !== 0) {
      console.error(`\`${command} ${args.join(' ')}\` failed`);
      return;
    }
  }

  if (useTypeScript) {
    verifyTypeScriptSetup();
  }

  if (tryGitInit(appPath)) {
    console.log();
    console.log('Initialized a git repository.');
  }

  // 显示最优雅的cd方式。
  // 这需要处理未定义的originalDirectory
  // 向后兼容旧的global-cli。
  let cdpath;
  if (originalDirectory && path.join(originalDirectory, appName) === appPath) {
    cdpath = appName;
  } else {
    cdpath = appPath;
  }

  // Change displayed command to yarn instead of yarnpkg
  const displayedCommand = useYarn ? 'yarn' : 'npm';

  console.log('xxxx....xxxxx');
}

初始化方法主要做的事情就是修改目标路径下的 package.json,添加一些配置命令,然后 copy!react-script 下的模板到目标路径下。

走到这一步,我们的项目基本已经初始化完成了。

所以我们 copy 了这么多 scripts

    start: 'react-scripts start',
    build: 'react-scripts build',
    test: 'react-scripts test',
    eject: 'react-scripts eject',

究竟是如何工作的呢,其实也不难,就是一些开发、测试、生产的环境配置。鉴于篇幅,咱就下一篇来分享下大佬们的前端构建的代码写法吧~~

总结

本来想用一张流程图解释下,但是。。。create-react-app 着实没有做啥!咱还是等下一篇分析完,自己写构建脚本的时候再画一下整体流程图(架构图)吧~

ok~ 简单概述下:

  • 判断 node 版本,如果大版本小于 8 ,则直接退出(截止目前是 8)
  • createReactApp.js 初始化一些命令参数,然后再去判断是否传入了 packageName,否则直接退出
  • 各种版本的判断,然后通过cross-spawn来用命令行执行所有的安装
  • 当所有的依赖安装完后,依旧通过命令行,初始化 node 环境,来执行 react-script 下的初始化方法:修改 package.json 中的一些配置、以及 copy 模板文件
  • 处理完成,给出用户友好提示

通篇看完 package 的职能后,发现,哇,这有点简答啊~~其实,我们学习源码的其实就是为了学习大佬们的一些边界情况处理,在后面自己开发的时候再去 copy~ 借鉴一些判断方法的编写。后面会再简单分析下react-scripts,然后写一个自己的一些项目架构脚本~

fluro介绍以及路由配置

前言

无论在react还是在vue中,都会有路由的配置,当然,他们一般都是单页面应用,但是对于开发而言,将路由统一管理,也无非是一个非常简洁方便的形式,甚至在不使用fluro的时候,Material也提供了onGenerateRoute 来配置路由,只不过那样会使入口页面非常的臃杂。所以这里的路由管理,我们使用 fluro

Fluro

我们在flutter package中搜索fluro,然后查看他的包详情

笔者喜欢直接转到GitHub去查看相关文档,因为里面会有example可以查看

example中,我们可以看到他的使用规范为在lib目录下新建一个config目录,在 application.dart文件中,配置总Router,同级目录下注册route,并且注册相关handle函数。在example下,我们可以查看到url传参,包括一般字段、link以及函数的传递。

具体的文档,大家可以自行到GitHub或者flutter package中自行查看。这里我们直接看在项目中的使用

配置项目Route

项目注入新package

  • pubspec.yaml
  dependencies:
    flutter:
      sdk: flutter
  
    # The following adds the Cupertino Icons font to your application.
    # Use with the CupertinoIcons class for iOS style icons.
    cupertino_icons: ^0.1.2
    dio: ^1.0.6
    fluro: "^1.3.7"

添加 fluro 依赖。

修改文件的目录名

观察flutter官网app或者example中,官方的命名规范,文件名都是以小写字母以及下划线组成,迎合官方标准,我们将项目中,我们自己的文件,曾经的驼峰命名法全部修改为小写字母+下划线的标准

file

配置路由

这里我们在lib目录下新建一个routers目录,仿着官方example的样子,配置我们的路由

  • lib/routers/application.dart
  import 'package:fluro/fluro.dart';
  
  class Application{
    static Router router;
  }

application中我们就注册一个总的Router,方便后面给Material中onGenerateRoute注册使用。

  • lib/routers/router_handler.dart
  import 'package:flutter/material.dart';
  import 'package:fluro/fluro.dart';
  import '../pages/article_detail.dart';
  
  Handler articleDetailHandler = Handler(
      handlerFunc: (
        BuildContext context, Map<String, List<String>> params) {
          String articleId = params['id']?.first;
          String title = params['title']?.first;
          print('index>,articleDetail id is $articleId');
          return ArticleDetail(articleId, title);
        }
  );

handle就是对于路由的处理函数,这里我们先注册一个对于详情页的处理函数,handlerFunc里面的params就是我们的url的查询参数。通过 Dart 的?.运算符可以“安全”的获取其参数值。最后return 我们需要跳转的页面。

对于我们不使用fluro的情况下 ,跳转页面就是 通过 Material 的 Navigator 跳转的,而传参呢,也就非常的代码"语义化"了,通过实例化对象而已。

  Navigator.of(context).push(new MaterialPageRoute(builder: 
      (BuildContext context) => new SidebarPage('First Page')));    //在new方法时调用控件的构造函数传入参数值
  • lib/routers/routes.dart
  import './router_handler.dart';
  import 'package:fluro/fluro.dart';
  import 'package:flutter/material.dart';
  
  class Routes {
    static String root = '/';
    static String articleDetail = "/detail";
  
    static void configureRoutes(Router router) {
      router.notFoundHandler = new Handler(
          handlerFunc: (BuildContext context, Map<String, List<String>> params) {
        print("ROUTE WAS NOT FOUND !!!");
      });
  
      router.define(articleDetail, handler: articleDetailHandler);
    }
  }

routes页面主要是讲路由以及handle函数组合起来,也是我们页面路由配置的入口文件,如上,我们暂时,只配置了notFoundHandler以及detail的页面。

  • lib/pages/my_app.dart

回到我们的项目入口文件,将我们写好的路由配置注册进去。

首先引入我们需要的配置文件

  import '../routers/routes.dart';
  import '../routers/application.dart';

在构造函数中去初始化我们的Routers

    final router = new Router();
    Routes.configureRoutes(router);
    Application.router = router;

最后在我们的MaterialApp中的onGenerateRoute中注入进入即可

   onGenerateRoute: Application.router.generator,

使用Router

首先,我们需要先写好我们的路由跳转目标页面,其实这个页面应该之前就写好,不然配置路由的时候怎么实例化呢是吧,莫方,现在补上也是么得问题的

  • lib/pages/article_detail.dart
  import 'package:flutter/material.dart';
  
  class ArticleDetail extends StatelessWidget {
    final String articleId;
    final String title;
  
    ArticleDetail(@required this.articleId, @required this.title);
  
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Center(
          child: Text("这篇文章的id是$articleId"),
        ),
      );
    }
  }

代码不做太多解释了,比较简单。接下来我们看下我们如何使用路由吧

跳转detail页面,当然是在点击首页list cell
的时候进行跳转,所以这里,我们需要给每一个cell添加一个点击监听

  • lib/wudgets/index_list_cell.dart

首先引入我们的application路由,以及dart的core包,因为涉及到url的传递,所以我们需要进行一次加密,否则会报错。

  import '../routers/application.dart';
  import 'dart:core'; 

由于需要加入点击监听,所以这里我们使用 InkWell widget 来包装下,关于 InkWell的更多介绍,大家可以查看相关文档:文档地址

    InkWell(
      onTap: () {
        print('跳转到详情页');
        Application.router.navigateTo(context, "/detail?id=${Uri.encodeComponent(cellInfo.detailUrl)}&title=${Uri.encodeComponent(cellInfo.title)}");
      },
      child:...
    )

对的,组合新的一行代码就是Application.router.navigateTo(context, "/detail?id=${Uri.encodeComponent(cellInfo.detailUrl)}&title=${Uri.encodeComponent(cellInfo.title)}"); 因为涉及中文以及url所以这里我们进行了Uri.encodeComponent

最终,我们即可看到我们的app已经可以跳转并且传参啦。

2detail

完整代码地址

总结

如上我们完成了页面路由的配置、跳转和传参,以及命名规范。至此,你应该学会

  • 查找Flutter package
  • 路由配置以及Material的路由配置
  • 不配置路由情况的跳转和传参
  • 官方demo命名规范(视团队规范而定)

网络请求

前言

无论对于一个什么app或者项目、应用来说,网络请求都是必不可少的,从前端的角度而言,有很多的网络请求库,比如 axios,fetch等,当然,也有ajax等等。对于flutter、dart而言,也是同样,下面,就让我们来看下在flutter中,我们如何进行网络请求。

Dart中的网络请求

http.dart 包

从dart原生的角度而言,它是提供了请求包的,package:http/http.dart 当然,也是非常的好用,我们完全可以将它封装下get和post然后使用

      import 'dart:async';
      import 'package:http/http.dart' as http;
      
      class NetUtils {
        // 参数拼接到url后面
        static Future<String> get(String url, {Map<String, String> params}) async {
          if (params != null && params.isNotEmpty) {
            // 如果参数不为空,则将参数拼接到URL后面
            StringBuffer sb = new StringBuffer("?");
            params.forEach((key, value) {
              sb.write("$key" + "=" + "$value" + "&");
            });
            String paramStr = sb.toString();
            paramStr = paramStr.substring(0, paramStr.length - 1);
            url += paramStr;
          }
          http.Response res = await http.get(url);
          return res.body;
        }
      
        // post请求
        static Future<String> post(String url, {Map<String, String> params}) async {
          http.Response res = await http.post(url, body: params);
          return res.body;
        }
      }

如上,我们就完成了对请求的封装,后面我们只需要在我们需要的地方使用post、get即可。

但是在这个项目中,我更倾向于使用dio ,他还有中文文档哦。
关键他已经帮我们封装了基本请求,不仅如此,还有拦截器、代理、cookie等。基本已经很全很方便了。

引入dio

首先在 pubspec.yaml 入驻相关包,这里不再赘述。在lib/util/文件夹下新建net_utils.dart文件用于封装一些请求方法

  • lib/util/net_utils.dart
      import 'package:dio/dio.dart';
      import 'dart:async';
      
      var dio = new Dio();
      
      class NetUtils {
        
        static Future get(String url,{Map<String,dynamic> params}) async{
           var response = await dio.get(url, data: params);
          return  response.data;
        }
      
        static Future post(String url,Map<String,dynamic> params) async{
          var response = await dio.post(url, data: params);
          return response.data;
        }
      }

还记得我们之前的约束嘛,index_page只想通过一个方法,拿到需要的数据,不要有太多累赘的数据处理,所以这里我们需要修改之前我们处理本地json中的方法

  • lib/util/data_utils.dart
    // 首页列表数据
    static Future<List<IndexCell>> getIndexListData(
        Map<String, dynamic> params) async {
      var response = await NetUtils.get(Api.RANK_LIST, params: params);
      var responseList = response['d']['entrylist'];
      List<IndexCell> resultList = new List();
      for (int i = 0; i < responseList.length; i++) {
        IndexCell cellData = new IndexCell.fromJson(responseList[i]);
        resultList.add(cellData);
      }
  
      return resultList;
    }

对于Api.RANK_LISK大家可以点击这里查看

至此,我们的准备工作就做好了,下面就来愉快使用这个方法吧。

  • lib/pages/index_page.dart
  const pageIndexArray = Constants.RANK_BEFORE;
...
  int _pageIndex = 0;
   
  getList(bool isLoadMore) {
    if(!isLoadMore){
      // reload的时候重置page
      _pageIndex = 0;
    }
    _params['before'] = pageIndexArray[_pageIndex];

    DataUtils.getIndexListData(_params).then((result) {
     setState(() {
        _listData = result;
      });
    });
  }

isLoadMore 从命名上即可看出,是判断是否为翻页,Constants.RANK_BEFORE 是我从web版掘金中,摘的几个翻页,这里不是pageNum为1、2、3 。。。翻页的,具体逻辑没有去思考,倒是这里我们只需要做成翻页即可。 私有变量 int _pageIndex = 0; 即为我们的page.

最后在页面初始化的时候,开始第一次的网络请求

  @override
  void initState() {
    super.initState();
    getList(false);
  }

现在,我们的数据就已经是线上的了。

img

总结

至此,我们完成了网络请求,并且已经封装了后面翻页的请求方法。你应该学会

  • Dart中的基础网络请求
  • dio的使用

Dart 从入门到放弃之语言概览

前言

原文链接:PersonalBlog

最近团队在利用业务时间开发Flutter开发者帮助App FlutterGo

img

img

不少同学希望能够出一个Dart的相关教程,由于Flutter Go使我们业余时间开发完成的,后续还是有很大的工作量和维护成本,所有关于Dart Go我们目前还是没有提上日程,作为补充,这里笔者希望能够总结官网和实践,给大家整理相关Dart语言的介绍。

环境准备

其实笔者在学习Dart也是主要通过Flutter来学习的,所以关于Dart的环境,笔者也是更加的推荐大家直接在网上操刀 dartpad,当然,对于已经配置好Flutter开发环境的同学可以直接在AS或者VSCode中直接新建Dart文件来运行,具体的配置和操作,这里不做过多介绍了。

Dart 中的 Hello world

下面的代码我们使用的是Dart中最基本的一些特性

  // 定义一个函数
  printInteger(int aNumber) {
    print('The number is $aNumber.'); // 控制台打印
  }
  
  // 程序运行的入口.
  main() {
    var number = 42; // 定义并赋值一个变量.
    printInteger(number); // 函数调用
  }

简单介绍下上面的程序:

// 这是一个注释

单行注释语句,当然,Dart同样支持文档注释和多行注释,后面我们会有介绍

int

数据类型,当然,Dart中还有很多别的数据类型,比如String、bool、List或者Map等

print

控制台打印输出,类似于浏览器中的 console.log

$variableName (or ${expression})

模板字符串,字符串中使用变量或者表达式,当然,如果不是表达式不推荐使用花括号。这一点区分于ES6

main()

这是一个非常重要、非常基础也必不可少的一个函数,他是整个程序的入口函数,这一点非常类似于C语言

var

一种声明变量不需要指定变量类型的方式,Dart 会自动判断赋值类型。

非常重要的几个概念

在学习Dart的过程中,有这么几个概念需要刻意记忆的

  • 万物皆对象!甚至包括number、functions或者null。而所有的对象又都继承自对象类
  • 虽然Dart是强类型语言,但是变量类型是可选的,因为Dart可以推断类型,在上面的代码中,number就可以被推断出是int类型,当你不想去显示的声明一个变量是什么类型的时候,可以使用特殊类型 dynamic
  • Dart 支持泛型,比如 List 代表一个只包含int类型的List,当然,我们也可以写成 List
  • Dart 支持顶级函数(如mian()),以及绑定到类或者对象的函数,通常称之为静态、实例方法。当然,我们还可以在函数中嵌套函数
  • 同样,Dart 也支持顶级变量,以及绑定到对象或者类上的变量。实例变量有时被称为字段或属性
  • 与java不同,Dart没有 public、protected和private关键字。如果变量以下划线开头,则为其库、类的私有变量。
  • 变量可以以字母或者下划线开头,后面可以是字母和数字的任意组合
  • Dart 拥有表达式和语句,例如条件表达式 condition?expr1:expr2; 的值为 expr1 或者 expr2 ,语句通常包含一个或者多个表达式,但是表达式不能直接包含语句
  • Dart工具可以报两种问题:警告和错误。警告通常是我们自己代码写的问题,它或阻止程序的运行,而错误可以编译时也可以是运行时。编译时错误会导致代码无法运行,而运行时错误则或导致程序异常

变量

下面是一个变量初始化和赋值的例子

  var name = 'Nealyang';

变量存储的是一个引用,在上面的例子中,名为name的变量包含一个对String 对象的引用,其值为 'Bob'

在这个例子中,name变量的类型被推断为 String,当时我们可以指定它来更改类型。如果对象不仅仅局限于的单个类型的时候,我们需要按照设计准则来指定对象或者动态类型。

dynamic name = 'Nealyang';

另一种选择是显式的声明变量类型

String name = 'Nealyang';

默认值

未初始化的变量初始值都是null,甚至是数字类型的变量在未初始化的时候也是null,毕竟在Dart中,一切皆为对象

  int lineCount;
  assert(lineCount == null);

assert 函数在生产环境下会被忽略 ,在开发环境中,当asset(condition)condition为false的时候,则会抛出一个异常

final & const

如果不打算更改一个变量,则可以考虑使用final 和 const 关键字,被final修饰的变量只能被赋值一次,而const修饰的变量是编译时常量。实例变量可以使final但是不能是const,final修饰的实例变量必须在构造函数之前初始化为最终实例变量

下面的例子是创建并且赋值与一个final修饰的变量:

  final name = 'Bob'; // 没有类型指定
  final String nickname = 'Bobby';

当我们尝试去修改的时候

name = 'Alice'; // Error: a final variable can only be set once.

对希望编译成常量的变量我们使用const,如果const变量在类里面,则将其标记为 static const。在声明变量的地方,将值设置为编译常量,当然也可以是一些常量算术运算的结果。

  const bar = 1000000; 
  const double atm = 1.01325 * bar; // 标准写法

当然,const关键字也不仅仅用于声明常量变量,我们还可以使用他来创建常量值。任何一个值都可以是常量值

var foo = const [];
final bar = const [];
const baz = []; // 等同于 `const []`

Nealyang 2018 前端路

介绍

国际惯例,先自我介绍下,我是Nealyang,16年毕业于东北大学,目前在阿里巴巴任职前端er。

2018-01-01 离职于环球网,离京后带着妹子来到了杭州,准确的说,离职后,在家里准备面试准备了三天,大致梳理了自己的技术专长(发现没有专长,尴尬了不是)。

来杭州陆陆续续面试的公司不多,其实抱着大厂梦,所以小公司的面试都是不怎么去的,找工作大概花了一个月的时间,主要是阿里的流程实在太长了,好在遇到现在的主管,各种帮我剑平催面试进度,让我面试后有很长一段时间过年去浪。

在杭州分别拿下网易、滴滴、阿里的offer,说实话,感觉运气成分还是相当大的。沉淀出一份面经《16年毕业的前端er在杭州求职ing》,里面的面试题没有具体总结是大厂的还是小厂,当时也是现在宾馆没事,随笔一些。。。没想到上了个热门。

目前就职于阿里拍卖,司法组前端开发一枚

我的 2018

关于生活

2018 真的比我想象中过的要累很多,很多计划也在中途被打乱过。

来到杭州,入职阿里后可能没多久就买了个房,目前正每个月1w+的还贷中。。。当然,也感谢妹子能够陪我到了上海(实习),创了北京,在拼到杭州。

gif

2019 可能就要结婚啦~~

关于个人呢,除了攒钱过日子学技术,还想好好锻炼身体~继续呼唤着六块腹肌,坚持每周能打一次篮球。

这里放一张刚租到蒋村时候随手拍的一张果照吧~~ (╯▽╰)

当然,也飞到过广州参加**首届React开发者大会,不能说收获满满,也是感慨万千。

最后呢,美滋滋的可能今年加了好多好多国内外大牛的微信(虽然没啥卵用),but who Who knows  ヘ(*–-)ノ

2018 沉淀

这里或许我们更应该专注于技术的分享,有遗憾也有收获

文章:

16年毕业的前端er在杭州求职ing

函数式编程了解一下(上)

窥探Underscore源码系列-开篇

函数式编程了解一下(下)

React源码分析与实现(一):组件的初始化与渲染

React源码分析与实现(二):状态、属性更新 -> setState

React源码分析与实现(三):实操DOM Diff

Flutter从入门到寄几玩儿

Flutter入门实战:从0到1仿写web版掘金App

Flutter开发者必备手册 Flutter Go

小册(十一月份写完,目前还在评审)

Flutter入门实战:从0到1仿写web版掘金App

github 开源

PersonalBlog 文章汇总

flutter 学习flutter的一些demo(还在更新中)

alibaba/flutter-go 参与开发的Flutter Go(alibaba group 下)

2018 的现实与梦想

遗憾

先说说2018心目中的遗憾吧,生活上或许最遗憾的是没有好好地出去旅游一次,技术上可能就太多了,先上一个2018年初定的目标吧

img

咳咳,惭愧了,90%没有做到,React源码阅读到20%左右的时候,被老板拉去研究学习了Flutter。项目强度大加上业务方面的需求,倒是react源码阅读部分就一直拖着了。函数式编程可能之算个说几个名字的“入门”,并不能再工作上熟练使用起来。。。别的源码阅读就连开始都没开始。。。

收获

虽然上面的目标基本都没有完成,也不代表2018就是完全失败的一年,刚刚加入阿里,阿里的生存法则还是需要时间去探索学习的。而现在,熟悉了这里一切的味道,工作上也挺开心愉快的,这也未尝不是一种收获。

关于Flutter也的确是意外收获吧,虽然它阻挡了我继续学习React的步伐,但是却打开了Dart、Flutter的大门,并产出一本小册、以及开源到Alibaba group 下的一个项目。还在开发中,还是有很多问题,但是心里还是美滋滋~ 而且,我也一直相信,Flutter 未来可期。

展望2019

哈哈,去年的目标那么多没有实现呢,展望啥呀还。。。不过,意思意思嘛~

认真的,2019 其实想做的事情可能真的有必要去搞一搞了

  • 学习TypeScript (这个是必须的)
  • 学习Nodejs,真正的运用到公司的项目中(研究型项目)
  • 重温JavaScript(你不知道的js,语言精粹,红宝书、忍者秘籍等),总结至少15篇JavaScript知识点的文章,不写重复之前写过的
  • 熟练掌握Flutter(未涉及到底层的功能,什么效果都应该能实现)

如果有时间和精力允许,想在学习一下数据结构和基本算法(每次学完一段时间就忘)。

而别的方面,坚持运动,保持良好的学习心态。认真工作、快乐生活吧~

最后

祝福大家在 2019 都能完成自己的新年目标。

“愿你有高跟鞋也有跑鞋,喝茶也喝酒。愿你有勇敢的朋友,有牛逼的对手。愿你对过往的一切情深意重,但从不回头。愿你特别美丽,特别平静,特别凶狠,也特别温柔。”

新年愿你 有盔甲,也有软肋。心中有傲骨,也有慈悲。有披着星辰黑着眼眶的夜,也有说走就走随时出发的旅程。

2019 一起加油!

面向大厂之JavaScript调用栈的那些事

前言

去年面试了很多前端的小伙伴,当然,结局是迟迟遇不到意中人~回头想想,其实前端面试,尤其是面试p6的题目,其实就那么些.

但话又说回来,就那么些。。。是哪么些呢?

在畅想自己未来技术方向的时候,愈发的感受到基础的重要性,所以想今年能够潜下心来,在学习新知识的同时能够总结所有,我认为我应该理解和掌握的知识。

原文地址链接:personal blog

调用栈

我们都知道,JavaScript 是一门单线程语言,也就是说,在同一时间,他只能做一件事,同时这意味着他只有一个调用栈。

而所谓的调用栈,其实就是一种数据结构,我们大概可以理解为LIFO的栈结构。而他做的事情也非常的简单,就是维护我们运行的函数(这样说不准确,后面解释)。暂时,我们可以将调用栈做的事情简单理解为,当我们运行一个函数的时候,他会将其放置到栈顶,当这个函数运行结束后,再从栈顶弹出。

让我们看下下面的代码运行:

function cat(name){

}

执行上下文

在介绍 JavaScript 的调用栈,其实不得不提到执行上下文,

Dart基础介绍

前言

Flutter的开发语言是Dart,就类似Android的开发语言是java一样,这个不需要过多介绍,本章节我们简单介绍下Dart中的一些语法,篇幅原因,不会谈及太深。笔者建议,官方Api浏览一遍即可,重在实操。涉及到不明白或者需要去查的部分可以再去翻阅文档。

这里我们会重点说下

文档推荐

Dart中文文档

Dart 官网

Dart api

关键概念

  • Dart中所有东西都是对象,每一个对象是类的实例,无论是数字、字符串、类还是函数
  • Dart是强类型语言,但是也只是var声明,因为Dart会推断变量类型
  • Dart支持通用类型,比如List<Map<String,String>>也可以写成List<Map<String,dynamic>>
  • Dart运行冲main函数开始,支持绑定到类或者对象的函数
  • Dart没有空开、私有的关键字,但是可以通过对象里下划线_开头去定义一个变量,则为私有变量

数据类型与修饰符

Dart语言支持如下数据类型

  • number
  • string
  • boolean
  • list
  • map
  • rune
  • symbol
int i  = 1;
double d = 1.1;
double e = 1.42e5;
// String -> int
var one = int.parse('1');
assert(one == 1);

// String -> double
var onePointOne = double.parse('1.1');
assert(onePointOne == 1.1);

// int -> String
String oneAsString = 1.toString();
assert(oneAsString == '1');

// double -> String
String piAsString = 3.14159.toStringAsFixed(2);
assert(piAsString == '3.14');

var s = 'string interpolation';
//创建多行字符串的方法:使用带有单引号或双引号的三重引号:
var s2 = """This is also a
multi-line string.""";

//只有两个对象具有bool类型:布尔字面量true和false,它们都是编译时常量
bool isLogin = false;
var hasMore = true;

//在Dart中,数组是列表对象
var list = [1, 2, 3];
//创建一个编译时常量列表,要在列表字面量之前添加const
List<int> constantList = const [1, 2, 3];
// constantList[1] = 1; // Uncommenting this causes an error.

// Map 在Dart中不是映射,而是对象
Map gifts = {
  // Key:    Value
  'first': 'partridge',
  'second': 'turtledoves',
  'fifth': 'golden rings'
};

Map gifts = Map();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';

函数

Dart是一种真正的面向对象语言,所以即使函数也是对象,具有类型和功能。这意味着函数可以分配给变量或作为参数传递给其他函数。您还可以像调用函数一样调用Dart类的实例

bool isNoble(int atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}
// 官方推荐标明返回值类型,我们也可以不写.当然,我们也可以使用箭头函数
isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

可选参数

可选参数可以是位置参数,也可以是命名参数,但不能两者都是。

void enableFlags({bool bold, bool hidden}) {...}
enableFlags(bold: true, hidden: false);
//Flutter实例创建表达式可能会变得复杂,因此小部件构造函数只使用命名参数。这使得实例创建表达式更容易阅读
const Scrollbar({Key key, @required Widget child})

可选的位置参数

在[]中包装一组函数参数,标记为可选的位置参数

String say(String from, String msg, [String device]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  return result;
}

默认参数值

void enableFlags({bool bold = false, bool hidden = false}) {...}
//bold 为true,hidden为false
enableFlags(bold: true);

词法作用域和闭包

bool topLevel = true;

void main() {
  var insideMain = true;

  void myFunction() {
    var insideFunction = true;

    void nestedFunction() {
      var insideNestedFunction = true;

      assert(topLevel);
      assert(insideMain);
      assert(insideFunction);
      assert(insideNestedFunction);
    }
  }
}

使用过es6的前端同学对这个应该很好理解

运算符和流程控制语句这里就省略了

类与泛型

基础知识

实用类成员

Point p = Point(2,2);//new 可有可无 -> Dart2
p.y = 3;//set
assert(p.y == 3);//get
//为避免最左操作数为空时出现异常,使用 ?.代替 .来使用:
p?.y = 4;

构造函数

class Point {
  num x;
  num y;

  Point(this.x, this.y);

  // Named constructor
  Point.fromJson(Map json) {
    x = json['x'];
    y = json['y'];
  }
}

获取对象类型

print('The type of a is ${a.runtimeType}');

构造函数

通过创建一个与类同名的函数来声明构造函数.构造函数最常见的应用形式是使用构造函数生成一个类的新实例.

如果不声明构造函数,则为您提供默认构造函数。默认构造函数没有参数,并在超类中调用无参数构造函数。子类不从父类继承构造函数。没有声明构造函数的子类只有默认的构造函数(没有参数,没有名称)而不是从父类继承的构造函数。

命名构造函数

使用命名构造函数可以在一个类中定义多个构造函数,或者让一个类的作用对于开发人员来说更清晰

class Point {
  num x, y;

  Point(this.x, this.y);

  // Named constructor
  Point.origin() {
    x = 0;
    y = 0;
  }
  
   // Named constructor
  Point.fromJson(Map json) {
    x = json['x'];
    y = json['y'];
  }
}

一定要记住构造函数是不会从父类继承的,这意味着父类的命名构造函数子类也不会继承。如果你希望使用在超类中定义的命名构造函数来创建子类,则必须在子类中实现该构造函数。

初始化列表

在构造函数主体运行之前初始化实例变量。初始值设定项用逗号分开。注意初始化器的右边部分中无法访问this关键字。
在开发期间,可以通过在初始化列表中使用assert来验证输入。

import 'dart:math';

class Point {
  final num x;
  final num y;
  final num distanceFromOrigin;

  Point(x, y)
      : x = x,
        y = y,
        distanceFromOrigin = sqrt(x * x + y * y);
}

main() {
  var p = new Point(2, 3);
  print(p.distanceFromOrigin);
}

///运行结果
3.605551275463989

常亮构造函数

如果类生成的对象不会改变,您可以使这些对象成为编译时常量。为此,定义一个const构造函数,并确保所有实例变量都是final的。

class ImmutablePoint {
  static final ImmutablePoint origin =
      const ImmutablePoint(0, 0);

  final num x, y;

  const ImmutablePoint(this.x, this.y);
}

工厂构造函数

在实现构造函数时使用factory关键字,该构造函数并不总是创建类的新实例。例如,工厂构造函数可以从缓存返回实例,也可以返回子类型的实例.这在我们后面介绍定义model很有用

class Logger {
  final String name;
  bool mute = false;

  // _cache is library-private, thanks to
  // the _ in front of its name.
  static final Map<String, Logger> _cache =
      <String, Logger>{};

  factory Logger(String name) {
    if (_cache.containsKey(name)) {
      return _cache[name];
    } else {
      final logger = Logger._internal(name);
      _cache[name] = logger;
      return logger;
    }
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}
//工厂构造函数不能访问this关键字。调用工厂构造函数,就像调用其他构造函数一样:
Logger logger = Logger('UI');
logger.log('Button clicked');

方法

import 'dart:math';

class Rectangle {
  num left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);
  
  //实例方法
  num distanceTo(Rectangle other) {
    var dx = width - other.width;
    var dy = height - other.height;
    return sqrt(dx * dx + dy * dy);
  }

  //set get
  // Define two calculated properties: right and bottom.
  num get right => left + width;
  set right(num value) => left = value - width;
  num get bottom => top + height;
  set bottom(num value) => top = value - height;
}

void main() {
  var rect = Rectangle(3, 4, 20, 15);
  assert(rect.left == 3);
  rect.right = 12;
  assert(rect.left == -8);
}

枚举类型

//使用enum关键字声明一个枚举类型:
enum Color { red, green, blue }
//枚举中的每个值都有一个索引getter,它返回enum声明中值的从0开始的位置
assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);
//要获取枚举中所有值的列表,请使用enum的values 常量
List<Color> colors = Color.values;
assert(colors[2] == Color.blue);
//您可以在switch语句中使用enum,如果switch的case不处理enum的所有值,将会报一个警告消息:
var aColor = Color.blue;

switch (aColor) {
  case Color.red:
    print('Red as roses!');
    break;
  case Color.green:
    print('Green as grass!');
    break;
  default: // 比如得有default,否则 WARNING.
    print(aColor); // 'Color.blue'
}

泛型

泛型通常是类型安全所必需的,他们对于写出严谨高质量的代码是很有用的:

  • 适当地指定泛型类型可以生成更好的代码
  • 可以使用泛型来减少代码重复
var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
var nameSet = Set<String>.from(names);

var views = Map<int, View>();

限制参数化类型

在实现泛型类型时,您可能希望限制其参数的类型。你可以使用extends。

class Foo<T extends SomeBaseClass> {
  // Implementation goes here...
  String toString() => "Instance of 'Foo<$T>'";
}

class Extender extends SomeBaseClass {...}
//可以使用SomeBaseClass 或它的任何子类作为泛型参数:
var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();

库与异步

import和library指令可以帮助您创建模块化和可共享的代码库。使用import来指定如何在另一个库的范围中使用来自一个库的命名空间。

导入一个库仅仅需要提供库的URI。对于内置库,URI具有特定的形式(dart:scheme)。对于其他库,可以使用文件路径或者包:scheme的形式。包:scheme形式指定包管理器(如pub工具)提供的库。

import 'dart:html';
import 'package:test/test.dart';

//指定一个库前缀 as
import 'package:lib2/lib2.dart' as lib2;
//如果您只想使用库的一部分,您可以有选择地导入库
// 只导入foo.
import 'package:lib1/lib1.dart' show foo;

// 除了foo 都导入.
import 'package:lib2/lib2.dart' hide foo;

懒加载库

延迟加载(也称为懒加载)允许应用程序在需要时按需加载库。以下是一些您可能使用延迟加载的情况:

  • 减少应用程序的初始启动时间
  • 例如,要执行A/B测试——尝试算法的其他实现
  • 加载很少使用的功能,如可选屏幕和对话框

要延迟加载库,必须首先使用deferred as进行导入。

import 'package:greetings/hello.dart' deferred as hello;

当您需要库时,使用库的标识符调用loadLibrary()。

Future greet() async {
  await hello.loadLibrary();
  hello.printGreeting();
}

注意:

  • 在导入文件中,递延库的常量不是常量。记住,这些常量在延迟库加载之前是不存在的。
  • 不能在导入文件中使用来自延迟库的类型
  • Dart隐式地将loadLibrary()插入到你定义使用deferred作为名称空间的名称空间中。函数的作用是:返回一个Future。

异步

async和await关键字支持异步编程,允许您编写类似于同步代码的异步代码。

处理Futures

当你需要一个完整的Futures的结果时,你有两个选择:

  • 使用async和await
  • 使用Future的API

使用async和await的代码虽然是异步的,但是看起来很像同步代码。要使用await必须是对一个使用async标注的异步函数:

Future checkVersion() async {
  try {//使用try,catch和finally来处理使用await的代码中的错误
    version = await lookUpVersion();
  } catch (e) {
    // React to inability to look up the version
  }
}

可以在异步函数中多次使用await

var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);

在await表达式中,表达式的值通常是一个Future对象。如果不是,那么这个值将被自动包装成Future。Futrue对象指示返回结果一定是一个对象。表达式的值就是被返回的对象。await表达式会让程序执行挂起,直到返回的对象可用。

结束语

以上大致总结与Dart中文文档,关于本项目主要涉及到的知识这里都有说明,但是并不包括Dart的全部知识,感兴趣的同学可以去官网学习学习,当然,我更喜欢的是,在项目中学习~ 下面让我们开始FLutter之旅吧~

flutter入门以及常用Widget介绍

前言

关于Flutter入门的文章非常多,常用Widget总结文章也非常多,之前写过相关入门文章,欢迎大家查看:flutter从入门到能寄几玩儿,这里我们还是以上面文章为主,先把项目跑起来,然后走马观花常用布局、常用Widget。

相关文章推荐

Flutter 中文网

Flutter 环境搭建

Flutter widget

Flutter走马观花

关于Flutter环境问题可以查阅上方链接,步骤都非常详细。
此后~大量代码来袭

基础Widget之material版Hello world

国际惯例,hello world

import 'package:flutter/material.dart';

class MyAppBar extends StatelessWidget{
  MyAppBar({this.title});//
  final Widget title;

  @override
  Widget build(BuildContext context){
    return new Container(
      height: 56.0,
      padding: const EdgeInsets.symmetric(horizontal:8.0),
      decoration: new BoxDecoration(
        color:Colors.blue[400]
      ),
      child: Row(
        children: <Widget>[
          new IconButton(
            icon:new Icon(Icons.menu),
            tooltip:'Navigation menu',
            onPressed: (){
              print('点击Menu');
            },
          ),
          new Expanded(
            child:new Center(
              child:title
            )
          ),
          new IconButton(
            icon:Icon(Icons.search),
            tooltip:'Search',
            onPressed: (){
              print('点击搜索按钮');
            },
          )
        ],
      ),
    );
  }
}

class MyScaffold extends StatelessWidget{
  @override 
  Widget build(BuildContext context){
    return Material(
      child: new Column(
        children:<Widget>[
          new MyAppBar(
            title:new Text(
              'Hello World',
              style:Theme.of(context).primaryTextTheme.title
             ),
          ),
          new Expanded(
            child:new Center(
              child:Text('Hello World!!!')
            )
          )
        ]
      ),
    );
  }
}

void main(){
  runApp(
    new MaterialApp(
      title:'My app',
      home:new MyScaffold()
    )
  );
}

img
代码地址:https://github.com/Nealyang/flutter

这个UI的确有些对不起人了,上面的title被挡住了。且先不去适配,后面我们使用Material提供的Scaffold即可

第一个例子,重点说下代码(用过的Widget记住):

  • 一切都是Widget,且Widget前面的new可有可无。
  • 类MyAppBar和MyScaffold中使用了Container、Row、Column、Text、IconButton、Icon、BoxDecoration、Center、Expanded等常用Widget
    • Container一个拥有绘制、定位、调整大小的 widget。类似于div,我们可以用它来创建矩形视图,container 可以装饰为一个BoxDecoration, 如 background、一个边框、或者一个阴影。 Container 也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。另外, Container可以使用矩阵在三维空间中对其进行变换。
    • RowColumn其实就是flex布局中的flex-direction
    • Expanded它会填充尚未被其他子项占用的的剩余可用空间。Expanded可以拥有多个children。然后使用flex参数来确定他们占用剩余空间的比例。更多细节可以参看:flutter控件Flexible和 Expanded的区别
  • 先定义了一个MyAppBar的类,构造函数中接受一个Widget的title,其实我们也可以接受String title然后在类中自己去new Title(title)
  • runApp函数接受给定的Widget并使用其作为widget根。
  • widget的主要工作是实现一个build函数,用以构建自身。一个widget通常由一些较低级别widget组成。Flutter框架将依次构建这些widget,直到构建到最底层的子widget时,这些最低层的widget通常为RenderObject,它会计算并描述widget的几何形状。

基本交互之material版Hello world

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // app的根Widget
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        // 这是设置的app主题
        // 运行后你可以看到app有一个蓝色的toobar,并且在不退出app的情况下修改代码会热更新
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

// 这是应用中一个基类,继承自StateFulWidget,意味着这个类拥有一个state对象,该对象里的一些字段会影响app的UI
// 这个类是state的一些配置项。通过构造函数来获取值,这个值一般在State中消费,并且使用final关键字。其实类似于react中的defaultProps

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      // setState方法告诉Flutter,这个State中有些值发生了变化,以便及时将新值更新到UI上,
      // 如果我不通过setState更改_count字段,那么Flutter并不会调用build匿名函数去更新界面
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // build方法会在每次setState的时候重新运行,例如上面的_incrementCounter方法被调用
    //Flutter已经被优化了重新构建的方法,所以你只会去更新需要去更新的部分,不必去单独更新里面的一些更细小的widget,类似于React中diff
    return new Scaffold(
      appBar: new AppBar(
        // 这里我们使用从App.build方法中初始化MyHomePage时候传入的title值来设置我们的title
        title: new Text(widget.title),
      ),
      body: new Center(
        // Center是一个布局Widget,他只有一个child(区分row or cloumn等是children),并且会将child的widget居中显示
        child: new Column(
          // Column也是一个布局widget,他可以有多个子widget
          // Column 有很多的属性去控制他的大小以及子widget的位置,这里我们使用mainAxisAlignment来让children在垂直线上居中,
          // 这里的主轴就是垂直的,因为Column就是垂直方向的,这里可以大概想象为display:flex,flex-directions:column,align-item,justifyContent。。。
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'Hello World!',
              style:TextStyle(
                fontSize:24.0,
                color: Colors.redAccent,
                decorationStyle:TextDecorationStyle.dotted,
                fontWeight: FontWeight.bold,
                fontStyle: FontStyle.italic,
                decoration: TextDecoration.underline
              )
            ),
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ),//最后这个逗号有利于格式化代码
    );
  }
}

img
注释上基本已经加了,这里重点说下,StatefulWidget和StatelessWidget.

  • Stateless widgets 是不可变的,这意味着它们的属性不能改变——所有的值都是 final
  • Stateful widgets 持有的状态可能在 widget 生命周期中发生变化,实现一个 stateful widget 至少需要两个类:1)一个 StatefulWidget 类;2)一个 State 类,StatefulWidget 类本身是不变的,但是 State 类在 widget 生命周期中始终存在
  • 如果需要变化需要重新创建。StatefulWidget可以保存自己的状态。那问题是既然widget都是immutable的,怎么保存状态?其实Flutter是通过引入了State来保存状态。当State的状态改变时,能重新构建本节点以及孩子的Widget树来进行UI变化。注意:如果需要主动改变State的状态,需要通过setState()方法进行触发,单纯改变数据是不会引发UI改变的。

还有关于key的部分这里就不做介绍了,其实就类似与react中key的概念,便于diff,提高效率的。
具体可以查看 Key

到这里,我们看到了Flutter的一些基本用法,Widget的套用、样式的编写、事件的注册,如果再学习下一些路由、请求、缓存是不是就可以自己开发APP了呢!

OK,强化下编写界面,咱再来些demo吧~

布局Widget

img

自己写的后,发现跟官网实现方式不同,代码地址

具体实现可以参照官网教程

这里不再赘述,下面我们说下对于布局的理解和感受以及常用布局widget。

从一个前端的角度来说,说到画界面,可能还是对布局这块比较敏感

img

img

当然,这里我们还是说下目前常用的flex布局,基本拿到页面从大到小拆分后就是如上图。

所以Widget布局其实也就是Row和Column用的最多,然后由于Flutter一切皆为组件的理念,可能会需要用到别的类css布局的Widget,譬如:Container。其实咱就理解为块元素吧!

下面简单演示下一些常用的Widget,这里就不在赘述Row和Column了。传送门:布局Widget

Container

可以添加padding、margin、border、background color、通常用于装饰其他Widget

img

代码链接 Nealyang/flutter

class MyHomePage extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    Container cell (String imgSrc){
      return new Container(
        decoration: new BoxDecoration(
          border:Border.all(width:6.0,color:Colors.black38),
          borderRadius: BorderRadius.all(const Radius.circular(8.0))
        ),
        child: Image.asset(
          'images/$imgSrc',
          width: 180.0,
          height: 180.0,
          fit: BoxFit.cover,
        ),
      );
    }

    return Container(
      padding: const EdgeInsets.all(10.0),
      color: Colors.grey,
      child: new Column(
        mainAxisSize: MainAxisSize.min,
        children:<Widget>[
          new Container(
            margin: const EdgeInsets.only(bottom:10.0),
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children:<Widget>[
                cell('1.jpg'),
                cell('2.jpg')
              ]
            ),
          ),
          new Container(
            margin: const EdgeInsets.only(bottom:10.0),
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children:<Widget>[
                cell('3.jpg'),
                cell('4.jpg')
              ]
            ),
          ),
        ]
      ),
    );
  }
}

该布局中每个图像使用一个Container来添加一个圆形的灰色边框和边距。然后使用容器将列背景颜色更改为浅灰色。

GridView

可滚动的网格布局,理解为display:grid

GridView提供两个预制list,当GridView检测到内容太长时,会自动滚动。如果需要构建自定义grid,可是使用GridView.countGridView.extent来指定允许设置的列数以及指定项最大像素宽度。

img

代码链接 Nealyang/flutter

List<Container> _buildGridTileList(int count) {

  return new List<Container>.generate(
      count,
      (int index) =>
          new Container(child: new Image.asset('images/${index+1}.jpg')));
}

Widget buildGrid() {
  return new GridView.extent(
      maxCrossAxisExtent: 150.0,
      padding: const EdgeInsets.all(4.0),
      mainAxisSpacing: 4.0,
      crossAxisSpacing: 4.0,
      children: _buildGridTileList(10));
}

class MyHomePage extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return  new Center(
        child: buildGrid(),
      );
  }
}

如上是指定maxCrossAxisExtent,我们可以直接去指定列数,例如官网的代码实例:

new GridView.count(
  primary: false,
  padding: const EdgeInsets.all(20.0),
  crossAxisSpacing: 10.0,
  crossAxisCount: 3,
  children: <Widget>[
    const Text('He\'d have you all unravel at the'),
    const Text('Heed not the rabble'),
    const Text('Sound of screams but the'),
    const Text('Who scream'),
    const Text('Revolution is coming...'),
    const Text('Revolution, they...'),
  ],
)

通过crossAxisCount直接指定列数。

Stack

层叠布局,position为absolute的感jio~

使用Stack来组织需要重叠的widget。widget可以完全或部分重叠底部widget。子列表中的第一个widget是base widget; 随后的子widget被覆盖在基础widget的顶部。Stack的内容不能滚动。有点类似于weex中的设置了absolute的感觉。底部组件永远在上面组件的上面。

ListView

可滚动的长列表,可以水平或者垂直。

Card

Material风格组件,卡片,AntD啥的组件库经常会出现的那种组件。

在flutter中,Card具有圆角和阴影,更改Card的elevation属性可以控制阴影效果。

ListTile

Material风格组件,我理解为常用的列表Item的样式,最多三行文字,可选的行前、行尾的图标

img

代码链接 Nealyang/flutter

总结

撇开框架设计原理和**不谈,其实技术就是一个工具,所谓实践出真知,我们需要的更多是掌握查阅这些widget的技巧以及常用widget在项目实践中的熟练掌握。

对于画界面,更多的还可以参看下官网教程:Flutter for Web开发者

下面让我们正式开始我们的《Flutter入门实战:从0到1仿写web版掘金App》吧!!!

小册 UI & 功能 编写

前言

如上,我们完成了首页、沸点的UI及功能的编写。小册这部分我们没有涉及到的知识点或许就是头部加上横切bar的效果。本篇完成,我们将实现如下效果:

img

TabBar & TabBarView

横切bar的效果,我们使用widget:TabBar + TabBarView。由于我们的顶部导航栏是不确定的(从接口获取数据),所以这里我们需要新建有状态类StatefulWidget,并且需要实例化 TabController 来设置顶部导航栏的个数。

  • lib/pages/book_page.dart
  @override
  Widget build(BuildContext context) {
    if (_navData.length == 0) {
        return Center(
          child: CircularProgressIndicator(),
        );
      }
    return new Scaffold(
      appBar: new AppBar(
        backgroundColor: Theme.of(context).primaryColor,
        title: new TabBar(
          controller: _tabController,
          tabs: _myTabs,
          indicatorColor: Colors.white,
          isScrollable: true,
        ),
      ),
      body: new TabBarView(
        controller: _tabController,
        children: _myTabView,
      ),
    );
  }
  • build 方法比较简单,一如既往的 Scaffold作为类似前端的HTML标签,撑开整个页面架构。
  • 但是这里title我们实例化了TabBar,controller是需要制定长度的一个TabController实例,tab为我们TabBar的子组件。indicatorColor 为tabBar下面当前tab的指示器。
  • TabBarView也跟Tab类似,注意,TabBar里面的tabs和TabBarView里面的children是一一对应关系。

数据准备

老规矩,我们继续去封装我们当前页面所需要的数据model以及数据请求的方法,但是这里,我们需要封装两套,因为tab是一套,tabView里面数据中的cell也需要一套。也就是上面我们说的TabBar和TabBarView需要一一对应的关系。

定义数据model

  • lib/model/book_nav.dart
  class BookNav {
    String name;
    String id;
    String alias;
  
    BookNav({this.alias, this.id, this.name});
  
    factory BookNav.fromJson(Map<String, dynamic> json) {
      return BookNav(alias: json['alias'], name: json['name'], id: json['id']);
    }
  }
  • lib/model/book_cell.dart
  class BookCell {
    String id;
    int sectionCount;
    int buyCount;
    String img;
    String title;
    String userName;
    double price;
  
    BookCell(
        {this.id,
        this.buyCount,
        this.img,
        this.price,
        this.title,
        this.sectionCount,
        this.userName});
  
    factory BookCell.fromJson(Map<String, dynamic> json) {
      return BookCell(
          id: json['_id'],
          buyCount: json['buyCount'],
          img: json['img'],
          title:json['title'],
          price: json['price'],
          sectionCount: json['section'].length,
          userName: json['userData']['username']);
    }
  }

这里就是定义我们界面每一个豆腐块需要的数据

请求数据

lib/api/api.dart 中,定义我们准备好的请求Url

  // 小册导航
  static const String BOOK_NAV = 'https://xiaoce-timeline-api-ms.juejin.im/v1/getNavList';
  static const String BOOK_LIST = 'https://xiaoce-timeline-api-ms.juejin.im/v1/getListByLastTime';
  • lib/util/net_util.dart
  // 获取小册导航栏
  static Future<List<BookNav>> getBookNavData() async {
    List<BookNav> resultList = [];
    var response = await NetUtils.get(Api.BOOK_NAV);
    var responseList = response['d'];
    for (int i = 0; i < responseList.length; i++) {
      BookNav bookNav;
      try {
        bookNav = BookNav.fromJson(responseList[i]);
      } catch (e) {
        print("error $e at $i");
        continue;
      }
      resultList.add(bookNav);
    }

    return resultList;
  }

  // 获取小册
  static Future<List<BookCell>> getBookListData(
      Map<String, dynamic> params) async {
    List<BookCell> resultList = new List();
    var response = await NetUtils.get(Api.BOOK_LIST, params: params);
    var responseList = response['d'];
    for (int i = 0; i < responseList.length; i++) {
      BookCell bookCell;
      try {
        bookCell = BookCell.fromJson(responseList[i]);
      } catch (e) {
        print("error $e at $i");
        continue;
      }
      resultList.add(bookCell);
    }

    return resultList;
  }

写到这里,细心地同学是否发现,我们data_utils.dart里面的代码有很多是重复的?是否可以抽象出来一个模子出来。当然,这里希望大家写完后仔细思考,有好的想法,欢迎在群里交流。

渲染页面

当我们拿到数据后,我们需要准备TabBar还需要准备TabBarView。

  List<Tab> _myTabs = <Tab>[
    Tab(
      text: '全部',
    )
  ];
  List<BookPageTabView> _myTabView = <BookPageTabView>[
    BookPageTabView(
      alias: 'all',
    )
  ];
    TabController _tabController;

initState的时候,请求数据,然后设置以上三个变量的值,去重新渲染页面。

  getNavList() {
    DataUtils.getBookNavData().then((resultData) {
      resultData.forEach((BookNav bn) {
        _myTabs.add(Tab(
          text: bn.name,
        ));
        _myTabView.add(BookPageTabView(
          alias: bn.alias,
        ));
      });
      if (this.mounted) {
        setState(() {
          _navData = resultData;
        });
        _tabController =
            new TabController(vsync: this, length: _navData.length + 1);
      }
    });
  }
  • 注意这里我们同样在setState之前判断了下当前页面是否monted,原因和之前说的一样。

TabBarView页面

如上,我们完成book_page的外壳,下面来编写TabBarView的页面内容

在pages目录下新建 book_page_tab_view.dart注意上面代码,我们在实例化tab_view page的时候是传递alias参数的,这也是我们请求当前tab下全部小册的重要参数。

  Widget _itemBuilder(context,index){
    return BookListCell(cellData: _bookList[index],);
  }

  @override
  Widget build(BuildContext context) {
    if (_bookList.length == 0) {
      return Center(
        child: CircularProgressIndicator(),
      );
    }
    return ListView.builder(
      itemBuilder: _itemBuilder,
      itemCount: _bookList.length,
    );
  }

页面请求逻辑大致和上面相同,我们依然将每一本小册的cell分离出来作为一个组件

  • lib/widget/book_list_cell.dart
@override
  Widget build(BuildContext context) {
    final Color accentColor = Theme.of(context).accentColor;
    final Color primaryColor = Colors.blueAccent;

    return InkWell(
      onTap: () {
        String url = "https://juejin.im/book/${cellData.id}";
        Application.router.navigateTo(context,
            "/web?url=${Uri.encodeComponent(url)}&title=${Uri.encodeComponent(cellData.title)}");
      },
      child: Container(
        padding: EdgeInsets.symmetric(
            horizontal: Util.setPercentage(0.03, context), vertical: 15.0),
        decoration: BoxDecoration(
          color: Colors.white,
          border: Border(
            bottom: BorderSide(color: accentColor, width: 0.5),
          ),
        ),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Container(
              padding: EdgeInsets.only(
                right: Util.setPercentage(0.03, context),
              ),
              child: Image.network(
                cellData.img,
                width: Util.setPercentage(0.2, context),
                height: 100,
                fit: BoxFit.contain,
              ),
            ),
            Container(
              width: Util.setPercentage(0.5, context),
              margin: EdgeInsets.only(
                  right: Util.setPercentage(0.01, context)), //0.8
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  Text(
                    cellData.title,
                    style: TextStyle(
                      color: Color(0xFF34383B),
                      fontSize: 18.0,
                      fontWeight: FontWeight.bold,
                    ),
                    maxLines: 3,
                    overflow: TextOverflow.ellipsis,
                  ),
                  SizedBox(
                    height: 5.0,
                  ),
                  Text(
                    cellData.userName,
                    style: TextStyle(color: Color(0xFF34383B), fontSize: 16.0),
                  ),
                  SizedBox(
                    height: 5.0,
                  ),
                  Row(
                    children: <Widget>[
                      Text(
                        '${cellData.sectionCount}小节',
                        style: TextStyle(color: accentColor),
                      ),
                      InTextDot(),
                      Text(
                        '${cellData.buyCount}人已购买',
                        style: TextStyle(color: accentColor),
                      )
                    ],
                  )
                ],
              ),
            ),
            Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: 15.0, vertical: 5.0),
                decoration: BoxDecoration(
                    color: Color(0xFFF0F7FF),
                    borderRadius: BorderRadius.all(Radius.circular(15.0))),
                child: Text(
                  '¥${cellData.price}',
                  style: TextStyle(color: primaryColor, fontSize: 16.0),
                ))
          ],
        ),
      ),
    );
  }

代码地址:flutter_juejin

总结

如上,我们完成了带有TabBar页面的UI以及功能的编写,后面活动、开源库的UI编写其实也是如此。使用的Widget基本都是我们常用的Widget,代码的编写风格也区域统一。

(译) 如何使用 React hooks 获取 api 接口数据

原文地址:robinwieruch
全文使用意译,不是重要的我就没有翻译了

在本教程中,我想向你展示如何使用 state 和 effect 钩子在React中获取数据。 你还将实现自定义的 hooks 来获取数据,可以在应用程序的任何位置重用,也可以作为独立节点包在npm上发布。

如果你对 React 的新功能一无所知,可以查看 React hooks 的相关 api 介绍。如果你想查看完整的如何使用 React Hooks 获取数据的项目代码,可以查看 github 的仓库

如果你只是想用 React Hooks 进行数据的获取,直接 npm i use-data-api 并根据文档进行操作。如果你使用他,别忘记给我个star 哦~

注意:将来,React Hooks 不适用于 React 中获取数据。一个名为Suspense的功能将负责它。以下演练是了解React中有关 state 和 Effect hooks 的更多信息的好方法。

使用 React hooks 获取数据

如果您不熟悉React中的数据提取,请查看我在React文章中提取的大量数据。 它将引导您完成使用React类组件的数据获取,如何使用Render Prop 组件和高阶组件来复用这些数据,以及它如何处理错误以及 loading 的。

  import React, { useState } from 'react';
  
  function App() {
    const [data, setData] = useState({ hits: [] });
  
    return (
      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    );
  }
  
  export default App;

App 组件显示了一个项目列表(hits=Hacker News 文章)。状态和状态更新函数来自useState 的 hook。他是来负责管理我们这个 data 的状态的。userState 中的第一个值是data 的初始值。其实就是个解构赋值。

这里我们使用 axios 来获取数据,当然,你也可以使用别的开源库。

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      'https://hn.algolia.com/api/v1/search?query=redux',
    );

    setData(result.data);
  });

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

这里我们使用 useEffect 的 effect hook 来获取数据。并且使用 useState 中的 setData 来更新组件状态。

但是如上代码运行的时候,你会发现一个特别烦人的循环问题。effect hook 的触发不仅仅是在组件第一次加载的时候,还有在每一次更新的时候也会触发。由于我们在获取到数据后就进行设置了组件状态,然后又触发了 effect hook。所以就会出现死循环。很显然,这是一个 bug!我们只想在组件第一次加载的时候获取数据 ,这也就是为什么你可以提供一个空数组作为 useEffect 的第二个参数以避免在组件更新的时候也触它。当然,这样的话,也就是在组件加载的时候触发。

  import React, { useState, useEffect } from 'react';
  import axios from 'axios';
  
  function App() {
    const [data, setData] = useState({ hits: [] });
  
    useEffect(async () => {
      const result = await axios(
        'https://hn.algolia.com/api/v1/search?query=redux',
      );
  
      setData(result.data);
    }, []);
  
    return (
      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    );
  }
  
  export default App;

第二个参数可以用来定义 hook 所依赖的所有变量(在这个数组中),如果其中一个变量发生变化,则就会触发这个 hook 的运行。如果传递的是一个空数组,则仅仅在第一次加载的时候运行。

是不是感觉 ,干了shouldComponentUpdate 的事情

这里还有一个陷阱。在这个代码里面,我们使用 async/await 去获取第三方的 API 的接口数据,根据文档,每一个 async 都会返回一个 promise:async 函数声明定义了一个异步函数,它返回一个 AsyncFunction 对象。异步函数是通过事件循环异步操作的函数,使用隐式的 Promise 返回结果 然而,effect hook 不应该返回任何内容,或者清除功能。这也就是为啥你看到这个警告:07:41:22.910 index.js:1452 Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect.. ``

这就是为什么我们不能在useEffect中使用 async的原因。但是我们可以通过如下方法解决:

  import React, { useState, useEffect } from 'react';
  import axios from 'axios';
  
  function App() {
    const [data, setData] = useState({ hits: [] });
  
    useEffect(() => {
      const fetchData = async () => {
        const result = await axios(
          'https://hn.algolia.com/api/v1/search?query=redux',
        );
  
        setData(result.data);
      };
  
      fetchData();
    }, []);
  
    return (
      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    );
  }
  
  export default App;

如上就是通过 React hooks 来获取 API 数据。但是,如果你对错误处理、loading、如何触发从表单中获取数据或者如何实现可重用的数据获取的钩子。请继续阅读。

如何自动或者手动的触发 hook? (How to trigger a hook programmatically/manually?)

目前我们已经通过组件第一次加载的时候获取了接口数据。但是,如何能够通过输入的字段来告诉 api 接口我对那个主题感兴趣呢?(就是怎么给接口传数据。这里原文说的有点啰嗦(还有 redux 关键字来混淆视听),我直接上代码吧)...

  ...

  function App() {
    const [data, setData] = useState({ hits: [] });
    const [query, setQuery] = useState('redux');
  
    useEffect(() => {
      const fetchData = async () => {
        const result = await axios(
          `http://hn.algolia.com/api/v1/search?query=${query}`,
        );
  
        setData(result.data);
      };
  
      fetchData();
    }, []);
  
    return (
      ...
    );
  }
  
  export default App;

这里我跳过一段,原文实在说的太细了。

缺少一件:当你尝试输入字段键入内容的时候,他是不会再去触发请求的。因为你提供的是一个空数组作为useEffect的第二个参数是一个空数组,所以effect hook 的触发不依赖任何变量,因此只在组件第一次加载的时候触发。所以这里我们希望当 query 这个字段一改变的时候就触发搜索

...

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );

      setData(result.data);
    };

    fetchData();
  }, [query]);

  return (
    ...
  );
}

export default App;

如上,我们只是把 query作为第二个参数传递给了 effect hook,这样的话,每当 query 改变的时候就会触发搜索。但是,这样就会出现了另一个问题:每一次的query 的字段变动都会触发搜索。如何提供一个按钮来触发请求呢?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [search, setSearch] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${search}`,
      );

      setData(result.data);
    };

    fetchData();
  }, [search]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button type="button" onClick={() => setSearch(query)}>
        Search
      </button>

      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

搜索的状态设置为组件的初始化状态,组件加载的时候就要触发搜索,类似的查询和搜索状态易造成混淆,为什么不把实际的 URL 设置为状态而不是搜索状态呢?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'https://hn.algolia.com/api/v1/search?query=redux',
  );

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(url);

      setData(result.data);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

这是一个使用 effect hook 来获取数据的一个例子,你可以决定 effect hook 所以依赖的状态。一旦你点击或者其他的什么操作 setState 了,那么 effect hook 就会运行。但是这个例子中,只有当你的 url 发生变化了,才会再次去获取数据。

在 Effect Hook 中使用 Loading(Loading Indicator with React Hooks)

这里让我们来给程序添加一个 loading(加载器),这里需要另一个 state

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'https://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);

      const result = await axios(url);

      setData(result.data);
      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;

代码比较简单,不解释了

使用 Effect Hook 添加错误处理(Error Handling with React Hooks)

如何在 Effect Hook 中做一些错误处理呢?错误仅仅是一个 state ,一旦程序出现了 error state,则组件需要去渲染一些feedback 给用户。当我们使用 async/await 的时候,我们可以使用try/catch,如下:

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'https://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      {isError && <div>Something went wrong ...</div>}

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;

每一次 effect hook 运行的时候都需要重置一下 error state,这是非常有必要的。因为用户可能想再发生错误的时候想再次尝试一下。

说白了,界面给用户反馈更加的友好

使用 React 中 Form 表单获取数据(Fetching Data with Forms and React)

function App() {
  ...

  return (
    <Fragment>
      <form onSubmit={event => {
        setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);

        event.preventDefault();
      }}>
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      ...
    </Fragment>
  );
}

为了防止浏览器的 reload,我们这里加了一个event.preventDefalut(),然后别的操作就是正常表单的操作了

自定义获取数据的 hook(Custom Data Fetching Hook)

其实就是请求的封装

为了能够提取自定义的请求 hook,除了属于输入框的 query 字段,别的包括 loading 加载器、错误处理函数都要包括在内。当然,你需要确保 App Component 所需的所有字段在你自定义的 hook 中都有返回

const useHackerNewsApi = () => {
  const [data, setData] = useState({ hits: [] });
  const [url, setUrl] = useState(
    'https://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return [{ data, isLoading, isError }, setUrl];
}

现在,我们可以将你的新 hook 继续放到组件中使用

function App() {
  const [query, setQuery] = useState('redux');
  const [{ data, isLoading, isError }, doFetch] = useHackerNewsApi();

  return (
    <Fragment>
      <form onSubmit={event => {
        doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);

        event.preventDefault();
      }}>
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      ...
    </Fragment>
  );
}

通常我们需要一个初始状态。将它简单的传递给自定义 hook 中

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

const useDataApi = (initialUrl, initialData) => {
  const [data, setData] = useState(initialData);
  const [url, setUrl] = useState(initialUrl);
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return [{ data, isLoading, isError }, setUrl];
};

function App() {
  const [query, setQuery] = useState('redux');
  const [{ data, isLoading, isError }, doFetch] = useDataApi(
    'https://hn.algolia.com/api/v1/search?query=redux',
    { hits: [] },
  );

  return (
    <Fragment>
      <form
        onSubmit={event => {
          doFetch(
            `http://hn.algolia.com/api/v1/search?query=${query}`,
          );

          event.preventDefault();
        }}
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;

如上,就是我们使用自定义 hook 来获取数据,该 hook 本身对 API 一无所知,它从外部接受所有的参数,但是仅管理重要的字段,比如 data、loading、error handler 等。它执行请求并且返回组件所需要的全部数据。

用于数据获取的 Reducer Hook(Reducer Hook for Data Fetching)

目前为止,我们使用各种 state hook 来管理数据、loading、error handler 等。然而,所有的这些状态,通过他们自己的状态管理,都属于同一个整体,因为他们所关心的数据状态都是请求相关的。正如你所看到的,他们都在 fetch 函数中使用。他们属于同一类型的另一个很好的表现就是在函数中,他们是一个接着一个被调用的(比如:setIsError、setIsLoading)。让我们用一个 Reducer Hook 来将这三个状态结合起来!

一个 Reducer Hook 返回一个状态对象和一个改变状态对象的函数。这个函数就是 dispatch function:带有一个 type 和参数的 action。

其实这些概念跟 redux 一毛一样

import React, {
  Fragment,
  useState,
  useEffect,
  useReducer,
} from 'react';
import axios from 'axios';

const dataFetchReducer = (state, action) => {
  ...
};

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  ...
};

Reducer Hook将reducer函数和初始状态对象作为参数。 在我们的例子中,数据,加载和错误状态的初始状态的参数没有改变,但它们已经聚合到一个由 reducer hook 而不是单个state hook 管理的状态对象。

const dataFetchReducer = (state, action) => {
  ...
};

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });

      try {
        const result = await axios(url);

        dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
      } catch (error) {
        dispatch({ type: 'FETCH_FAILURE' });
      }
    };

    fetchData();
  }, [url]);

  ...
};

现在,在获取数据的时候,可以使用 dispathc function 来给reducer传递参数。使用dispatch函数发送的对象具有必需的type属性和可选的payload属性。该类型告诉reducer功能需要应用哪个状态转换,并且reducer可以另外使用有效负载来提取新状态。毕竟,我们只有三个状态转换:初始化提取过程,通知成功的数据提取结果,并通知错误的数据提取结果。

在我们自定义的 hook 中,state 像以前一样返回。但是因为我们有一个状态对象而不是独立状态。 这样,调用useDataApi自定义钩子的人仍然可以访问数据,isLoading和isError:

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  ...

  return [state, setUrl];
};

最后还有我们 reducer 函数的实现。它需要作用于三个不同的状态转换,称为FETCH_INIT,FETCH_SUCCESS和FETCH_FAILURE。 每个状态转换都需要返回一个新的状态对象。 让我们看看如何使用switch case语句实现它:

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state,
        isLoading: true,
        isError: false
      };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      };
    case 'FETCH_FAILURE':
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    default:
      throw new Error();
  }
};

现在,每一个 action 都有对应的处理,并且返回一个新的 state。

总之,Reducer Hook确保状态管理的这一部分用自己的逻辑封装。此外,你永远不会遇到无效状态。例如,以前可能会意外地将isLoading和isError状态设置为true。 在这种情况下,UI应该显示什么?现在,reducer函数定义的每个状态转换都会导致一个有效的状态对象。

在 Effect Hook 中 中止数据请求(Abort Data Fetching in Effect Hook)

React中的一个常见问题是,即使组件已经卸载(例如由于使用React Router导航),也会设置组件状态。我之前已经在这里写过关于这个问题的文章,它描述了如何防止在各种场景中为未加载的组件中设置状态。 让我们看看我们如何阻止在数据提取的自定义钩子中设置状态:

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
    let didCancel = false;

    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });

      try {
        const result = await axios(url);

        if (!didCancel) {
          dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
        }
      } catch (error) {
        if (!didCancel) {
          dispatch({ type: 'FETCH_FAILURE' });
        }
      }
    };

    fetchData();

    return () => {
      didCancel = true;
    };
  }, [url]);

  return [state, setUrl];
};

每一个 Effect Hook 都自带一个清理功能。该功能在组件卸载时运行。清理功能是 hook 返回的一个功能。在我们的例子中,我们使用一个名为 didCancel 的 boolean 来标识组件的状态。如果组件已卸载,则该标志应设置为true,这将导致在最终异步解析数据提取后阻止设置组件状态。

注意:实际上不会中止数据获取 - 这可以通过Axios Cancellation实现 - 但是对于 unmounted 的组件不再执行状态转换。 由于Axios Cancellation在我看来并不是最好的API,因此这个防止设置状态的布尔标志也能完成这项工作。

学习交流

关注公众号: 【全栈前端精选】 每日获取好文推荐。还可以入群,一起学习交流呀~~

下拉刷新 & 加载更多

前言

目前,我们的app首页已经有个样子出来了,并且数据也不是开玩笑的,都是真实数据,对于首页而言,我们当然还需要添加翻页的功能,甚至包括下拉刷新的功能。幸运是的,Flutter对于这方面来说,给我们封装的还是相当的便利的。

修复上面请求接口bug

今天重新打开页面的时候发现了页面报错,debug了一下发现是接口数据过来有些字段为空导致的。遂这里请求接口以及数据的处理上,做了一些调整

  • lib/util/data_utils.dart
      try {
        IndexCell cellData = new IndexCell.fromJson(responseList[i]);
        resultList.add(cellData);
      } catch (e) {
        // No specified type, handles all
        print('Something really unknown: $i');
      }

getIndexListData 方法里面加了一层异常捕获,毕竟不是我们自己跟后端的约束,对业务也并非清楚,所以里面一些字段我们也不清楚,这些问题也是难免。

  • lib/model/indexCell.dart
  factory IndexCell.fromJson(Map<String, dynamic> json) {
    String _tag = '';
    if(json['tags'].length>0){
      _tag = '${json['tags'][0]['title']}/';
    }
    return IndexCell(
      hot: json['hot'],
      collectionCount: json['collectionCount'],
      commentCount: json['commentsCount'],
      tag: '$_tag${json['category']['name']}',
      username: json['user']['username'],
      createdTime: Util.getTimeDuration(json['createdAt']),
      title: json['title'],
      detailUrl: json['originalUrl'],
      isCollection: json['type'] ,
    );
  }

添加了对tag的为空判断

下拉刷新

下拉刷新其实就是在我们之前调用下我们的getList方法。难点可能就是如何触发这个下拉刷新呢?非常幸运,Material 中 提供了 RefreshIndicator widget

同样,我们可以从他的源码中看到他的属性

img

  • lib/index_page.dart
  // 下拉刷新
  Future<void> _onRefresh() async{//The RefreshIndicator onRefresh callback must return a Future.
    _listData.clear();
    setState(() {
      _listData = _listData;
      //注意这里需要重置一切请求条件
      _hasMore = true;
    });
    getList(false);
    return null;
  }
  //build 方法返回如下
    return RefreshIndicator(
      onRefresh: _onRefresh,
      child: ListView.builder(
        itemCount: _listData.length + 2, //添加一个header 和 loadMore
        itemBuilder: (context, index) => _renderList(context, index),
      ),
    );

下拉刷新的方法中setState是为了重新build,毕竟空列表页面会有loading的出现。

loadMore的实现

  • lib/index_page.dart
    ListView.builder中有一个属性为Controller,他可以监听页面的滚动,所以我们在ListView.builder中加入这个属性
  ScrollController _scrollController = new ScrollController();
  

  // build方法中
    return RefreshIndicator(
      onRefresh: _onRefresh,
      child: ListView.builder(
        itemCount: _listData.length + 2, //添加一个header 和 loadMore
        itemBuilder: (context, index) => _renderList(context, index),
         controller: _scrollController,
      ),
    );

在页面初始化的时候键入滚动监听,并且触发getList方法

  @override
    void initState() {
      super.initState();
      getList(false);
      _scrollController.addListener(() {
        if (_scrollController.position.pixels ==
            _scrollController.position.maxScrollExtent) {
              print('loadMore');
          getList(true);
        }
      });
    }

一些别的变量的作用,由于触底这个动作可以反复触发,而网络请求是异步的,所以我们不可能在每一次触底都要去发送过一次请求,而是再一次请求结束后,再次触底才会再次发送请求。所以这里我们加入了 _isRequesting 的flag

  bool _isRequesting = false; //是否正在请求数据的flag
  bool _hasMore = true;

请求的方法也做了稍微的跳转

  getList(bool isLoadMore) {
    if (_isRequesting || !_hasMore) return;
    if (!isLoadMore) {
      // reload的时候重置page
      _pageIndex = 0;
    }
    _params['before'] = pageIndexArray[_pageIndex];
    _isRequesting = true;
    DataUtils.getIndexListData(_params).then((result) {
      _pageIndex += 1;
      List<IndexCell> resultList = new List();
      if(isLoadMore){
        resultList.addAll(_listData);
      }
      resultList.addAll(result);
      setState(() {
        _listData = resultList;
        _hasMore = _pageIndex < pageIndexArray.length;
        _isRequesting = false;
      });
    });
  }

添加加载器

页面触底,发送请求,讲道理应该是要给用户一个反馈的,由于这个可以很多页面公用,所以当然,我们将其封装为一个widget

  • lib/widgets/load_more.dart
    import 'package:flutter/material.dart';
    
    class LoadMore extends StatelessWidget {
      final bool hasMore;
    
      LoadMore(this.hasMore);
    
      @override
      Widget build(BuildContext context) {
        if (hasMore) {
          return Container(
            height: 70.0,
            child: Center(
              child: Opacity(
                opacity: 1.0,
                child: CircularProgressIndicator(
                  strokeWidth: 3.0,
                ),
              ),
            ),
          );
        }
        return Container(
          height: 70.0,
          child: Center(
            child: Text('亲,我也是有底线的',
                style: TextStyle(color: Theme.of(context).accentColor)),
          ),
        );
      }
    }

最终,我们的页面效果就出来了。 代码地址

loadMore

总结

通过这一章节,你应该学会

  • Dart中异常的处理和捕获
  • 下拉刷新
  • 加载更多

React源码分析与实现(三):实操DOM Diff

原文链接:Nealyang PersonalBlog

由于源码中diff算法掺杂了太多别的功能模块,并且dom diff相对于之前的代码实现来说还是有些麻烦的,尤其是列表对比的算法,所以这里我们单独拿出来说他实现

前言

众所周知,React中最为人称赞的就是Virtual DOM和 diff 算法的完美结合,让我们可以不顾性能的“任性”更新界面,前面文章中我们有介绍道Virtual DOM,其实就是通过js来模拟dom的实现,然后通过对js obj的操作,最后渲染到页面中,但是,如果当我们修改了一丢丢东西,就要渲染整个页面的话,性能消耗还是非常大的,如何才能准确的修改该修改的地方就是我们diff算法的功能了。

其实所谓的diff算法大概就是当状态发生改变的时候,重新构造一个新的Virtual DOM,然后根据与老的Virtual DOM对比,生成patches补丁,打到对应的需要修改的地方。

这里引用司徒正美的介绍

最开始经典的深度优先遍历DFS算法,其复杂度为O(n^3),存在高昂的diff成本,然后是cito.js的横空出世,它对今后所有虚拟DOM的算法都有重大影响。它采用两端同时进行比较的算法,将diff速度拉高到几个层次。紧随其后的是kivi.js,在cito.js的基出提出两项优化方案,使用key实现移动追踪及基于key的编辑长度距离算法应用(算法复杂度 为O(n^2))。但这样的diff算法太过复杂了,于是后来者snabbdom将kivi.js进行简化,去掉编辑长度距离算法,调整两端比较算法。速度略有损失,但可读性大大提高。再之后,就是著名的vue2.0 把snabbdom整个库整合掉了。

与传统diff对比

传统的diff算法通过循环递归每一个节点,进行对比,这样的操作效率非常的低,复杂程度O(n^3),其中n标识树的节点总数。如果React仅仅是引入传统的diff算法的话,其实性能也是非常差的。然而FB通过大胆的策略,满足了大多数的性能最大化,将O(n^3)复杂度的问题成功的转换成了O(n),并且后面对于同级节点移动,牺牲一定的DOM操作,算法的复杂度也才打到O(max(M,N))。

img

实现思路

这里借用下网上的一张图,感觉画的非常赞~

img

大概解释下:

额。。。其实上面也已近解释了,当Virtual DOM发生变化的时,如上图的第二个和第三个 p 的sonx被删除了,这时候,我们就通过diff算法,计算出前后Virtual DOM的差异->补丁对象patches,然后根据这个patches对象中的信息来遍历之前的老Virtual DOM树,对其需要更新的地方进行更新,使其变成新VIrtual DOM。

diff 策略

  • Web UI中节点跨级操作特别少,可以忽略不计

  • 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。(哪怕一样的而我也认为不一样 -> 大概率优化)

  • 对于同一层级的一组子节点,他们可以通过唯一的key来区分,以方便后续的列表对比算法

基于如上,React分别对tree diff、Component diff 、element diff 进行了算法优化。

tree diff

基于策略一,React的diff非常简单明了:只会对同一层次的节点进行比较。这种非传统的按深度遍历搜索,这种通过大胆假设得到的改进方案,不仅符合实际场景的需要,而且大幅降低了算法实现复杂度,从O(n^3)提升至O(n)。

基于此,React官方并不推荐进行DOM节点的跨层级操作 ,倘若真的出现了,那就是非常消耗性能的remove和create的操作了。

我是真的不会画图

img

Component diff

由于React是基于组件开发的,所以组件的dom diff其实也非常简单,如果组件是同一类型,则进行tree diff比较。如果不是,则直接放入到patches中。即使是子组件结构类型都相同,只要父组件类型不同,都会被重新渲染。这也说明了为什么我们推荐使用shouldComponentUpdate来提高React性能。

大概的感觉是酱紫的

IMAGE

list diff

对于节点的比较,其实只有三种操作,插入、移动和删除。(这里最麻烦的是移动,后面会介绍实现)。当被diff节点处于同一层级时,通过三种节点操作新旧节点进行更新:插入,移动和删除,同时提供给用户设置key属性的方式调整diff更新中默认的排序方式,在没有key值的列表diff中,只能通过按顺序进行每个元素的对比,更新,插入与删除,在数据量较大的情况下,diff效率低下,如果能够基于设置key标识尽心diff,就能够快速识别新旧列表之间的变化内容,提升diff效率。

对于这三种理论知识可以参照知乎上不可思议的 react diff的介绍。

IMAGE

算法实现

前方高清多码预警

diff

这里引入代码处理我们先撇开list diff中的移动操作,先一步一步去实现

根据节点变更类型,我们定义如下几种变化

const ATTRS = 'ATTRS';//属性改变
const TEXT = 'TEXT';//文本改变
const REMOVE = 'REMOVE';//移除操作
const REPLACE = 'REPLACE';//替换操作

let  Index = 0;

解释下index,为了方便演示diff,我们暂时没有想react源码中给每一个Element添加唯一标识


var ReactElement = function(type, key, ref, self, source, owner, props) {
  var element = {
    // This tag allow us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,//重点在这里

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner,
  };

  
  return element;
};

...


'use strict';

// The Symbol used to tag the ReactElement type. If there is no native Symbol
// nor polyfill, then a plain number is used for performance.
var REACT_ELEMENT_TYPE =
  (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
  0xeac7;

module.exports = REACT_ELEMENT_TYPE;

我们遍历每一个VDom,以index为索引。注意这里我们使用全局变量index,因为遍历整个VDom,以index作为区分,所以必须用全局变量,当然,GitHub上有大神的实现方式为{index:0},哈引用类型传递,换汤不换药

开始遍历

export default function diff(oldTree, newTree) {
    let patches = {};
    // 递归树, 比较后的结果放到补丁包中
    walk(oldTree, newTree, Index, patches)
    return patches;
}
function walk(oldNode, newNode, index, patches) {
    let currentPatch = [];

    if(!newNode){
        currentPatch.push({
            type:REMOVE,
            index
        });
    }else if(isString(oldNode) && isString(newNode)){
        if(oldNode !== newNode){// 判断是否为文本
            currentPatch.push({
                type:TEXT,
                text:newNode
            });
        }
    }else if (oldNode.type === newNOde.type) {
        // 比较属性是否有更改
        let attrs = diffAttr(oldNode.porps, newNode.props);
        if (Object.keys(attrs).length > 0) {
            currentPatch.push({
                type: ATTRS,
                attrs
            });
        }

        // 比较儿子们
        diffChildren(oldNode.children,newNode.children,patches);
    }else{
        // 说明节点被替换
        currentPatch.push({
            type: REPLACE,
            newNode
        });
    }

    currentPatch.length ? patches[index] = currentPatch : null;
}

function diffChildren(oldChildren,newChildren,patches) {  
    oldChildren.forEach((child,ids)=>{
        // index 每次传递给walk时, index应该是递增的.所有的都基于同一个Index
        walk(child,newChildren[idx],++Index,patches);
    })
}

function diffAttr(oldAttrs, newAttrs) {
    let patch = {};
    // 判断老属性和新属性的关系
    for (let key in oldAttrs) {
        if (oldAttrs[key] !== newAttrs[key]) {
            patch[key] = newAttrs[key]; //有可能是undefined => 新节点中删了该属性
        }
    }

    // 新节点新增了很多属性
    for (let key in newAttrs) {
        if (!oldAttrs.hasOwnProperty(key)) {
            patch[key] = newAttrs[key];
        }
    }

    return patch;
}

在diff过程中,我们需要去判断文本标签,需要在util中写一个工具函数

function isString(node) { 
    return Object.prototype.toString.call(node)==='[object String]';
 }

实现思路非常简单,手工流程图了解下

img

通过diff后,最终我们会拿到新旧VDom的patches补丁,补丁的内容大致如下:

patches = {
  1:{
    type:'REMOVE',
    index:1
  },
  3:{
    type:'TEXT',
    newText:'hello Nealyang~',
  },
  6:{
    type:'REPLACE',
    newNode:newNode
  }
}

大致是这么个感觉,两秒钟体会下~

这里应该会有点诧异的是1 3 6...是什么鬼?

因为之前我们说过,diff采用的依旧是深度优先遍历,及时你是改良后的升级产品,但是遍历流程依旧是:

img

patches

既然patches补丁已经拿到了,该如何使用呢,对,我们依旧是遍历!

Element 调用render后,我们已经可以拿到一个通过VDom(代码)解析后的真是Dom了,所以我们只需要将遍历真实DOM,然后在指定位置修改对应的补丁上指定位置的更改就行了。

代码如下:(自己实现的简易版)

let allPaches = {};
let index = 0; //默认哪个需要补丁
export default function patch(dom, patches) {
    allPaches = patches;
    walk(dom);
}

function walk(dom) {
    let currentPatche = allPaches[index];
    let childNodes = dom.childNodes;
    childNodes.forEach(element => walk(element));
    if (currentPatche > 0) {
        doPatch(dom, currentPatche);
    }
}

function doPatch(node, patches) {
    patches.forEach(patch => {
        switch (patch.type) {
            case 'ATTRS':
                setAttrs(patch.attrs)//别的文件方法
                break;
            case 'TEXT':
                node.textContent = patch.text;
                break;
            case 'REPLACE':
                let newNode = patch.newNode instanceof Element ? render(patch.newNode) : document.createTextNode(patch.newNode);
                node.parentNode.replaceChild(newNode, node)
                break;
            case 'REMOVE':
                node.parentNode.removeChild(node);
                break;
        }
    })
}

关于setAttrs其实功能都加都明白,这里给个简单实例代码,大家YY下

function setAttrs(dom, props) {
    const ALL_KEYS = Object.keys(props);

    ALL_KEYS.forEach(k =>{
        const v = props[k];

        // className
        if(k === 'className'){
            dom.setAttribute('class',v);
            return;
        }
        if(k == "style") {
            if(typeof v == "string") {
                dom.style.cssText = v
            }

            if(typeof v == "object") {
                for (let i in v) {
                    dom.style[i] =  v[i]
                }
            }
            return
        }

        if(k[0] == "o" && k[1] == "n") {
            const capture = (k.indexOf("Capture") != -1)
            dom.addEventListener(k.substring(2).toLowerCase(),v,capture)
            return
        }

        dom.setAttribute(k, v)
    })
}

如上,其实我们已经实现了DOM diff了,但是存在一个问题.

如下图,老集合中包含节点:A、B、C、D,更新后的新集合中包含节点:B、A、D、C,此时新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。

IMAGE

针对这一现象,React 提出优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,虽然只是小小的改动,性能上却发生了翻天覆地的变化!

具体介绍可以参照 https://zhuanlan.zhihu.com/p/20346379

这里我们放到代码实现上:

/**
 * Diff two list in O(N).
 * @param {Array} oldList - Original List
 * @param {Array} newList - List After certain insertions, removes, or moves
 * @return {Object} - {moves: <Array>}
 *                  - moves is a list of actions that telling how to remove and insert
 */
function diff (oldList, newList, key) {
    var oldMap = makeKeyIndexAndFree(oldList, key)
    var newMap = makeKeyIndexAndFree(newList, key)
  
    var newFree = newMap.free
  
    var oldKeyIndex = oldMap.keyIndex
    var newKeyIndex = newMap.keyIndex
  
    var moves = []
  
    // a simulate list to manipulate
    var children = []
    var i = 0
    var item
    var itemKey
    var freeIndex = 0
  
    // first pass to check item in old list: if it's removed or not
    // 遍历旧的集合
    while (i < oldList.length) {
      item = oldList[i]
      itemKey = getItemKey(item, key)//itemKey a
      // 是否可以取到
      if (itemKey) {
        // 判断新集合中是否有这个属性,如果没有则push null
        if (!newKeyIndex.hasOwnProperty(itemKey)) {
          children.push(null)
        } else {
          // 如果有 去除在新列表中的位置
          var newItemIndex = newKeyIndex[itemKey]
          children.push(newList[newItemIndex])
        }
      } else {
        var freeItem = newFree[freeIndex++]
        children.push(freeItem || null)
      }
      i++
    }

// children [{id:"a"},{id:"b"},{id:"c"},null,{id:"e"}]
  
    var simulateList = children.slice(0)//[{id:"a"},{id:"b"},{id:"c"},null,{id:"e"}]
  
    // remove items no longer exist
    i = 0
    while (i < simulateList.length) {
      if (simulateList[i] === null) {
        remove(i)
        removeSimulate(i)
      } else {
        i++
      }
    }
  
    // i is cursor pointing to a item in new list
    // j is cursor pointing to a item in simulateList
    var j = i = 0
    while (i < newList.length) {
      item = newList[i]
      itemKey = getItemKey(item, key)//c
  
      var simulateItem = simulateList[j] //{id:"a"}
      var simulateItemKey = getItemKey(simulateItem, key)//a
  
      if (simulateItem) {
        if (itemKey === simulateItemKey) {
          j++
        } else {
          // 新增项,直接插入
          if (!oldKeyIndex.hasOwnProperty(itemKey)) {
            insert(i, item)
          } else {
            // if remove current simulateItem make item in right place
            // then just remove it
            var nextItemKey = getItemKey(simulateList[j + 1], key)
            if (nextItemKey === itemKey) {
              remove(i)
              removeSimulate(j)
              j++ // after removing, current j is right, just jump to next one
            } else {
              // else insert item
              insert(i, item)
            }
          }
        }
      } else {
        insert(i, item)
      }
  
      i++
    }
  
    //if j is not remove to the end, remove all the rest item
    var k = simulateList.length - j
    while (j++ < simulateList.length) {
      k--
      remove(k + i)
    }
  
  
    // 记录旧的列表中移除项 {index:3,type:0}
    function remove (index) {
      var move = {index: index, type: 0}
      moves.push(move)
    }
  
    function insert (index, item) {
      var move = {index: index, item: item, type: 1}
      moves.push(move)
    }
  
    // 删除simulateList中null
    function removeSimulate (index) {
      simulateList.splice(index, 1)
    }
  
    return {
      moves: moves,
      children: children
    }
  }
  
  /**
   * Convert list to key-item keyIndex object.
   * 将列表转换为 key-item 的键值对象
   * [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}] -> [a:0,b:1,c:2...]
   * @param {Array} list
   * @param {String|Function} key
   */
  function makeKeyIndexAndFree (list, key) {
    var keyIndex = {}
    var free = []
    for (var i = 0, len = list.length; i < len; i++) {
      var item = list[i]
      var itemKey = getItemKey(item, key)
      if (itemKey) {
        keyIndex[itemKey] = i
      } else {
        free.push(item)
      }
    }
    return {
      keyIndex: keyIndex,
      free: free
    }
  }
  
  // 获取置顶key的value
  function getItemKey (item, key) {
    if (!item || !key) return void 666
    return typeof key === 'string'
      ? item[key]
      : key(item)
  }
  
  exports.makeKeyIndexAndFree = makeKeyIndexAndFree 
  exports.diffList = diff

代码参照:list-diff 具体的注释都已经加上。
使用如下:

import {diffList as diff} from './lib/diffList';

var oldList = [{id: "a"}, {id: "b"}, {id: "c"}, {id: "d"}, {id: "e"}]
var newList = [{id: "c"}, {id: "a"}, {id: "b"}, {id: "e"}, {id: "f"}]

var moves = diff(oldList, newList, "id")
// type 0 表示移除, type 1 表示插入
// moves: [
//   {index: 3, type: 0},
//   {index: 0, type: 1, item: {id: "c"}}, 
//   {index: 3, type: 0}, 
//   {index: 4, type: 1, item: {id: "f"}}
//  ]
console.log(moves)
moves.moves.forEach(function(move) {
  if (move.type === 0) {
    oldList.splice(move.index, 1) // type 0 is removing
  } else {
    oldList.splice(move.index, 0, move.item) // type 1 is inserting
  }
})

// now `oldList` is equal to `newList`
// [{id: "c"}, {id: "a"}, {id: "b"}, {id: "e"}, {id: "f"}]
console.log(oldList) 

img

这里我最困惑的地方时,实现diff都是index为索引,深度优先遍历,如果存在这种移动操作的话,那么之前我补丁patches里记录的index不就没有意义了么??

在 后来在开源的simple-virtual-dom中找到了index作为索引和标识去实现diff的答案。

  • 第一点:在createElement的时候,去记录每一元素children的count数量
function Element(tagName, props, children) {
    if (!(this instanceof Element)) {
        if (!_.isArray(children) && children != null) {
            children = _.slice(arguments, 2).filter(_.truthy)
        }
        return new Element(tagName, props, children)
    }

    if (_.isArray(props)) {
        children = props
        props = {}
    }

    this.tagName = tagName
    this.props = props || {}
    this.children = children || []
    this.key = props ?
        props.key :
        void 666

    var count = 0

    _.each(this.children, function (child, i) {
        if (child instanceof Element) {
            count += child.count
        } else {
            children[i] = '' + child
        }
        count++
    })

    this.count = count
}
  • 第二点,在diff算法中,遇到移动的时候,我们需要及时更新我们全局变量index,核心代码(leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1。完整代码如下:
function diffChildren(oldChildren, newChildren, index, patches, currentPatch) {
    var diffs = diffList(oldChildren, newChildren, 'key')
    newChildren = diffs.children

    if (diffs.moves.length) {
        var reorderPatch = {
            type: patch.REORDER,
            moves: diffs.moves
        }
        currentPatch.push(reorderPatch)
    }

    var leftNode = null
    var currentNodeIndex = index
    _.each(oldChildren, function (child, i) {
        var newChild = newChildren[i]
        currentNodeIndex = (leftNode && leftNode.count) ?
            currentNodeIndex + leftNode.count + 1 :
            currentNodeIndex + 1
        dfsWalk(child, newChild, currentNodeIndex, patches)
        leftNode = child
    })
}

话说,这里困扰了我好久好久。。。。

img

回到开头

var REACT_ELEMENT_TYPE =
  (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
  0xeac7;

也就说明了这段代码的必要性。

0.3中diff的实现

最后我们在看下0.3中diff的实现:

 updateMultiChild: function(nextChildren, transaction) {
    if (!nextChildren && !this._renderedChildren) {
      return;
    } else if (nextChildren && !this._renderedChildren) {
      this._renderedChildren = {}; // lazily allocate backing store with nothing
    } else if (!nextChildren && this._renderedChildren) {
      nextChildren = {};
    }
    var rootDomIdDot = this._rootNodeID + '.';
    var markupBuffer = null;  // Accumulate adjacent new children markup.
    var numPendingInsert = 0; // How many root nodes are waiting in markupBuffer
    var loopDomIndex = 0;     // Index of loop through new children.
    var curChildrenDOMIndex = 0;  // See (Comment 1)
    
    for (var name in nextChildren) {
      if (!nextChildren.hasOwnProperty(name)) {continue;}

      // 获取当前节点与要渲染的节点
      var curChild = this._renderedChildren[name];
      var nextChild = nextChildren[name];

      // 是否两个节点都存在,且类型相同
      if (shouldManageExisting(curChild, nextChild)) {
        // 如果有插入标示,之后又循环到了不需要插入的节点,则直接插入,并把插入标示制空
        if (markupBuffer) {
          this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
          markupBuffer = null;
        }
        numPendingInsert = 0;

        // 如果找到当前要渲染的节点序号比最大序号小,则移动节点
        /*
         * 在0.3中,没有根据key做diff,而是通过Object中的key作为索引
         * 比如{a,b,c}替换成{c,b,c}
         * b._domIndex = 1挪到loopDomIndex = 1的位置,就是原地不动
           a._domIndex = 0挪到loopDomIndex = 2的位置,也就是和c换位
        */ 
        if (curChild._domIndex < curChildrenDOMIndex) { // (Comment 2)
          this.enqueueMove(curChild._domIndex, loopDomIndex);
        }
        curChildrenDOMIndex = Math.max(curChild._domIndex, curChildrenDOMIndex);

        // 递归更新子节点Props,调用子节点dom-diff...
        !nextChild.props.isStatic &&
          curChild.receiveProps(nextChild.props, transaction);
        curChild._domIndex = loopDomIndex;
      } else {
        // 当前存在,执行删除
        if (curChild) {               // !shouldUpdate && curChild => delete
          this.enqueueUnmountChildByName(name, curChild);
          curChildrenDOMIndex =
            Math.max(curChild._domIndex, curChildrenDOMIndex);
        }
        // 当前不存在,下个节点存在, 执行插入,渲染下个节点
        if (nextChild) {              // !shouldUpdate && nextChild => insert
          this._renderedChildren[name] = nextChild;
          // 渲染下个节点
          var nextMarkup =
            nextChild.mountComponent(rootDomIdDot + name, transaction);
          markupBuffer = markupBuffer ? markupBuffer + nextMarkup : nextMarkup;
          numPendingInsert++;
          nextChild._domIndex = loopDomIndex;
        }
      }
      loopDomIndex = nextChild ? loopDomIndex + 1 : loopDomIndex;
    }

    // 执行插入操作,插入位置计算方式如下:
    // 要渲染的节点位置-要插入的节点个数:比如当前要渲染的节点index=3,当前节点只有一个,也就是index=1。
    // 如<div>1</div>渲染成<div>1</div><div>2</div><div>3</div>
    // 那么从<div>2</div>开始就开始加入buffer,最终buffer内容为<div>2</div><div>3</div>
    // 那么要插入的位置为 3 - 1 = 2。我们以<div>1</div>为1,就是把buffer插入2的位置,也就是<div>1</div>后面
    if (markupBuffer) {
      this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
    }

    // 循环老节点
    for (var childName in this._renderedChildren) { 
      if (!this._renderedChildren.hasOwnProperty(childName)) { continue; }
      var child = this._renderedChildren[childName];

      // 当前节点存在,下个节点不存在,删除
      if (child && !nextChildren[childName]) {
        this.enqueueUnmountChildByName(childName, child);
      }
    }
    // 一次提交所有操作
    this.processChildDOMOperationsQueue();
  }

【THE LAST TIME】this:call、apply、bind

前言

The last time, I have learned

【THE LAST TIME】一直是我想写的一个系列,旨在厚积薄发,重温前端。

也是给自己的查缺补漏和技术分享。

欢迎大家多多评论指点吐槽。

系列文章均首发于公众号【全栈前端精选】,笔者文章集合详见Nealyang/personalBlog。目录皆为暂定

讲道理,这篇文章有些拿捏不好尺度。准确的说,这篇文章讲解的内容基本算是基础的基础了,但是往往这种基础类的文章很难在啰嗦和详细中把持好。文中道不到的地方还望各位评论多多补充指正。

THE LAST TIME 系列

This

相信使用过 JavaScript 库做过开发的同学对 this 都不会陌生。虽然在开发中 this 是非常非常常见的,但是想真正吃透 this,其实还是有些不容易的。包括对于一些有经验的开发者来说,也都要驻足琢磨琢磨~ 包括想写清楚 this 呢,其实还得聊一聊 JavaScript 的作用域和词法

This 的误解一:this 指向他自己

function foo(num) {
  console.log("foo:"+num);
  this.count++;
}

foo.count = 0;

for(var i = 0;i<10;i++){
    foo(i);
}

console.log(foo.count);

通过运行上面的代码我们可以看到,foo函数的确是被调用了十次,但是this.count似乎并没有加到foo.count上。也就是说,函数中的this.count并不是foo.count

This 的误解二:this 指向他的作用域

另一种对this的误解是它不知怎么的指向函数的作用域,其实从某种意义上来说他是正确的,但是从另一种意义上来说,这的确是一种误解。

明确的说,this不会以任何方式指向函数的词法作用域,作用域好像是一个将所有可用标识符作为属性的对象,这从内部来说他是对的,但是JavaScript代码不能访问这个作用域“对象”,因为它是引擎内部的实现

function foo() {
    var a = 2;
    this.bar();
}

function bar() {
    console.log( this.a );
}

foo(); //undefined

全局环境中的 This

既然是全局环境,我们当然需要去明确下宿主环境这个概念。简而言之,一门语言在运行的时候需要一个环境,而这个环境的就叫做宿主环境。对于 JavaScript 而言,宿主环境最为常见的就是 web 浏览器。

如上所说,我们也可以知道环境不是唯一的,也就是 JavaScript 代码不仅仅可以在浏览器中跑,也能在其他提供了宿主环境的程序里面跑。另一个最为常见的就是 Node 了,同样作为宿主环境node 也有自己的 JavaScript 引擎:v8.

  • 浏览器中,在全局范围内,this 等价于 window 对象
  • 浏览器中,用 var 声明一个变量等价于给 this 或者 window 添加属性
  • 如果你在声明一个变量的时候没有使用var或者let(ECMAScript 6),你就是在给全局的this添加或者改变属性值
  • 在 node 环境里,如果使用 REPL 来执行程序,那么 this 就等于 global
  • 在 node 环境中,如果是执行一个 js 脚本,那么 this 并不指向 global 而是module.exports{}
  • 在node环境里,在全局范围内,如果你用REPL执行一个脚本文件,用var声明一个变量并不会和在浏览器里面一样将这个变量添加给this
  • 如果你不是用REPL执行脚本文件,而是直接执行代码,结果和在浏览器里面是一样的
  • node环境里,用REPL运行脚本文件的时候,如果在声明变量的时候没有使用var或者let,这个变量会自动添加到global对象,但是不会自动添加给this对象。如果是直接执行代码,则会同时添加给globalthis

这一块代码比较简单,我们不用码说话,改为用图说话吧!

函数、方法中的 This

很多文章中会将函数和方法区分开,但是我觉得。。。没必要啊,咱就看谁点了如花这位菇凉就行

当一个函数被调用的时候,会建立一个活动记录,也成为执行环境。这个记录包含函数是从何处(call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的this引用。

函数中的 this 是多变的,但是规则是不变的。

你问这个函数:”~~老妹~~~ oh,不,函数!谁点的你?“

”是他!!!“

那么,this 就指向那个家伙!再学术化一些,所以!一般情况下!this不是在编译的时候决定的,而是在运行的时候绑定的上下文执行环境。this 与声明无关!

function foo() {
    console.log( this.a );
}

var a = 2;

foo(); // 2

记住上面说的,谁点的我!!! => foo() = windwo.foo(),所以其中this 执行的是 window 对象,自然而然的打印出来 2.

需要注意的是,对于严格模式来说,默认绑定全局对象是不合法的,this被置为undefined。

function foo() {
    console.log( this.a );
}

var obj2 = {
    a: 42,
    foo: foo
};

var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42

虽然这位 xx 被点的多了。。。但是,我们只问点他的那个人,也就是 ojb2,所以 this.a输出的是 42.

注意,我这里的点!不是你想的那个点哦,是运行时~

构造函数中的 This

恩。。。这,就是从良了

还是如上文说到的,this,我们不看在哪定义,而是看运行时。所谓的构造函数,就是关键字new打头!

谁给我 new,我跟谁

其实内部完成了如下事情:

  • 一个新的对象会被创建
  • 这个新创建的对象会被接入原型链
  • 这个新创建的对象会被设置为函数调用的this绑定
  • 除非函数返回一个他自己的其他对象,这个被new调用的函数将自动返回一个新创建的对象
foo = "bar";
function testThis(){
  this.foo = 'foo';
}
console.log(this.foo);
new testThis();
console.log(this.foo);
console.log(new testThis().foo)//自行尝试

call、apply、bind 中的 this

恩。。。这就是被包了

在很多书中,call、apply、bind 被称之为 this 的强绑定。说白了,谁出力,我跟谁。那至于这三者的区别和实现以及原理呢,咱们下文说!

function dialogue () {
  console.log (`I am ${this.heroName}`);
}
const hero = {
  heroName: 'Batman',
};
dialogue.call(hero)//I am Batman

上面的dialogue.call(hero)等价于dialogue.apply(hero)``dialogue.bind(hero)().

其实也就是我明确的指定这个 this 是什么玩意儿!

箭头函数中的 this

箭头函数的 this 和 JavaScript 中的函数有些不同。箭头函数会永久地捕获 this值,阻止 apply或 call后续更改它。

let obj = {
  name: "Nealyang",
  func: (a,b) => {
      console.log(this.name,a,b);
  }
};
obj.func(1,2); // 1 2
let func = obj.func;
func(1,2); //   1 2
let func_ = func.bind(obj);
func_(1,2);//  1 2
func(1,2);//   1 2
func.call(obj,1,2);// 1 2
func.apply(obj,[1,2]);//  1 2

箭头函数内的 this值无法明确设置。此外,使用 call 、 apply或 bind等方法给 this传值,箭头函数会忽略。箭头函数引用的是箭头函数在创建时设置的 this值。

箭头函数也不能用作构造函数。因此,我们也不能在箭头函数内给 this设置属性。

class 中的 this

虽然 JavaScript 是否是一个面向对象的语言至今还存在一些争议。这里我们也不去争论。但是我们都知道,类,是 JavaScript 应用程序中非常重要的一个部分。

类通常包含一个 constructor , this可以指向任何新创建的对象。

不过在作为方法时,如果该方法作为普通函数被调用, this也可以指向任何其他值。与方法一样,类也可能失去对接收器的跟踪。

class Hero {
  constructor(heroName) {
    this.heroName = heroName;
  }
  dialogue() {
    console.log(`I am ${this.heroName}`)
  }
}
const batman = new Hero("Batman");
batman.dialogue();

构造函数里的 this指向新创建的 类实例。当我们调用 batman.dialogue()时, dialogue()作为方法被调用, batman是它的接收器。

但是如果我们将 dialogue()方法的引用存储起来,并稍后将其作为函数调用,我们会丢失该方法的接收器,此时 this参数指向 undefined 。

const say = batman.dialogue;
say();

出现错误的原因是JavaScript 类是隐式的运行在严格模式下的。我们是在没有任何自动绑定的情况下调用 say()函数的。要解决这个问题,我们需要手动使用 bind()将 dialogue()函数与 batman绑定在一起。

const say = batman.dialogue.bind(batman);
say();

this 的原理

咳咳,技术文章,咱们严肃点

我们都说,this指的是函数运行时所在的环境。但是为什么呢?

我们都知道,JavaScript 的一个对象的赋值是将地址赋值给变量的。引擎在读取变量的时候其实就是要了个地址然后再从原地址读出来对象。那么如果对象里属性也是引用类型的话(比如 function),当然也是如此!

截图自阮一峰博客

而JavaScript 允许函数体内部,引用当前环境的其他变量,而这个变量是由运行环境提供的。由于函数又可以在不同的运行环境执行,所以需要个机制来给函数提供运行环境!而这个机制,也就是我们说到心在的 this。this的初衷也就是在函数内部使用,代指当前的运行环境。

var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

截图自阮一峰博客

obj.foo()是通过obj找到foo,所以就是在obj环境执行。一旦var foo = obj.foo,变量foo就直接指向函数本身,所以foo()就变成在全局环境执行.

总结

  • 函数是否在new中调用,如果是的话this绑定的是新创建的对象
var bar = new Foo();
  • 函数是否通过call、apply或者其他硬性调用,如果是的话,this绑定的是指定的对象
var bar = foo.call(obj);
  • 函数是否在某一个上下文对象中调用,如果是的话,this绑定的是那个上下文对象
var bar = obj.foo();
  • 如果都不是的话,使用默认绑定,如果在严格模式下,就绑定到undefined,注意这里是方法里面的严格声明。否则绑定到全局对象
var bar = foo();

小试牛刀

var number = 2;
var obj = {
  number: 4,
  /*匿名函数自调*/
  fn1: (function() {
    var number;
    this.number *= 2; //4

    number = number * 2; //NaN
    number = 3;
    return function() {
      var num = this.number;
      this.number *= 2; //6
      console.log(num);
      number *= 3; //9
      alert(number);
    };
  })(),

  db2: function() {
    this.number *= 2;
  }
};

var fn1 = obj.fn1;

alert(number);

fn1();

obj.fn1();

alert(window.number);

alert(obj.number);

评论区留下你的答案吧~

call & applay

上文中已经提到了 callapplybind,在 MDN 中定义的 apply 如下:

apply() 方法调用一个函数, 其具有一个指定的this值,以及作为一个数组(或类似数组的对象)提供的参数

语法:

fun.apply(thisArg, [argsArray])

  • thisArg:在 fun 函数运行时指定的 this 值。需要注意的是,指定的 this 值并不一定是该函数执行时真正的 this 值,如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。
  • argsArray:一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 fun 函数。如果该参数的值为null 或 undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。浏览器兼容性请参阅本文底部内容。

如上概念 apply 类似.区别就是 apply 和 call 传入的第二个参数类型不同。

call 的语法为:

fun.call(thisArg[, arg1[, arg2[, ...]]])

需要注意的是:

  • 调用 call 的对象,必须是个函数 Function
  • call 的第一个参数,是一个对象。 Function 的调用者,将会指向这个对象。如果不传,则默认为全局对象 window。
  • 第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 Function 的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到 Function 对应的第一个参数上,之后参数都为空。

apply 的语法为:

Function.apply(obj[,argArray])

需要注意的是:

  • 它的调用者必须是函数 Function,并且只接收两个参数
  • 第二个参数,必须是数组或者类数组,它们会被转换成类数组,传入 Function 中,并且会被映射到 Function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。

记忆技巧:apply,a 开头,array,所以第二参数需要传递数据。

请问!什么是类数组?

核心理念

借!

对,就是借。举个栗子!我没有女朋友,周末。。。额,不,我没有摩托车🏍,周末的时候天气很好,想出去压弯。但是我有没有钱!怎么办呢,找朋友借用一下啊~达到了目的,还节省开支!

放到程序中我们可以理解为,某一个对象没有想用的方法去实现某个功能,但是不想浪费内存开销,就借用另一个有该方法的对象去借用一下。

说白了,包括 bind,他们的核心理念都是借用方法,已达到节省开销的目的。

应用场景

代码比较简单,就不做讲解了

  • 将类数组转换为数组
const arrayLike = {
  0: 'qianlong',
  1: 'ziqi',
  2: 'qianduan',
  length: 3
}
const arr = Array.prototype.slice.call(arrayLike);

运行结果

  • 求数组中的最大值
var arr = [34,5,3,6,54,6,-67,5,7,6,-8,687];
Math.max.apply(Math, arr);
Math.max.call(Math, 34,5,3,6,54,6,-67,5,7,6,-8,687);
Math.min.apply(Math, arr);
Math.min.call(Math, 34,5,3,6,54,6,-67,5,7,6,-8,687);
  • 变量类型判断

Object.prototype.toString用来判断类型再合适不过,尤其是对于引用类型来说。

function isArray(obj){
  return Object.prototype.toString.call(obj) == '[object Array]';
}
isArray([]) // true
isArray('qianlong') // false
  • 继承
// 父类
function supFather(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green']; // 复杂类型
}
supFather.prototype.sayName = function (age) {
    console.log(this.name, 'age');
};
// 子类
function sub(name, age) {
    // 借用父类的方法:修改它的this指向,赋值父类的构造函数里面方法、属性到子类上
    supFather.call(this, name);
    this.age = age;
}
// 重写子类的prototype,修正constructor指向
function inheritPrototype(sonFn, fatherFn) {
    sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法
    sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上
}
inheritPrototype(sub, supFather);
sub.prototype.sayAge = function () {
    console.log(this.age, 'foo');
};
// 实例化子类,可以在实例上找到属性、方法
const instance1 = new sub("OBKoro1", 24);
const instance2 = new sub("小明", 18);
instance1.colors.push('black')
console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}
console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18} 

继承后面可能也会写一个篇【THE LAST TIME】。也是比较基础,不知道有没有这个必要

简易版继承

ar Person = function (name, age) {
  this.name = name;
  this.age = age;
};
var Girl = function (name) {
  Person.call(this, name);
};
var Boy = function (name, age) {
  Person.apply(this, arguments);
}
var g1 = new Girl ('qing');
var b1 = new Boy('qianlong', 100);

bind

bind 和 call/apply 用处是一样的,但是 bind 会**返回一个新函数!不会立即执行!**而call/apply改变函数的 this 并且立即执行。

应用场景

  • 缓存参数

原理其实就是返回闭包,毕竟 bind 返回的是一个函数的拷贝

for (var i = 1; i <= 5; i++) {
    // 缓存参数
    setTimeout(function (i) {
        console.log('bind', i) // 依次输出:1 2 3 4 5
    }.bind(null, i), i * 1000);
}

上述代码也是一个经典的面试题,具体也不展开了。

  • this 丢失问题

说道 this 丢失问题,应该最常见的就是 react 中定义一个方法然后后面要加 bind(this)的操作了吧!当然,箭头函数不需要,这个咱们上面讨论过。

手写实现

apply

第一个手写咱们一步一步来

  • 从定义触发,因为是 function 调用者。所以肯定是给 function 添加方法咯,并且第一个参数是未来 this 上下文
Function.prototype.NealApply = function(context,args){}
  • 如果context,this 指向 window
Function.prototype.NealApply = function(context,args){
    context = context || window;
    args = args || [];
}
  • 给 context 新增一个不可覆盖的 key,然后绑定 this

对,我们没有黑魔法,既然绑定 this,还是逃不掉我们上文说的那些 this 方式

Function.prototype.NealApply = function(context,args){
    context = context || window;
    args = args || [];
    //给context新增一个独一无二的属性以免覆盖原有属性
    const key = Symbol();
    context[key] = this;//这里的 this 是函数
    context[key](...args);
}

其实这个时候我们用起来已经有效果了。

  • 这个时候我们已经执行完了,我们需要将结果返回,并且清理自己产生的垃圾
Function.prototype.NealApply = function(context,args){
    context = context || window;
    args = args || [];
    //给context新增一个独一无二的属性以免覆盖原有属性
    const key = Symbol();
    context[key] = this;//这里的 this 是 testFun
    const result = context[key](...args);
    // 带走产生的副作用
    delete context[key];
    return result;
}

var name = 'Neal'

function testFun(...args){
    console.log(this.name,...args);
}

const testObj = {
    name:'Nealyang'
}

testFun.NealApply(testObj,['一起关注',':','全栈前端精选']);

执行结果就是上方的截图。

  • 优化

一上来不说优化是因为希望大家把精力放到核心,然后再去修边幅! 罗马不是一日建成的,看别人的代码多牛批,其实也是一点一点完善出来的。

道理是这么个道理,其实要做的优化还有很多,这里我们就把 context 的判断需要优化下:

    // 正确判断函数上下文对象
    if (context === null || context === undefined) {
       // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
        context = window 
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }

别的优化大家可以添加各种的用户容错。比如对第二个参数的类数组做个容错

    function isArrayLike(o) {
        if (o &&                                    // o不是null、undefined等
            typeof o === 'object' &&                // o是对象
            isFinite(o.length) &&                   // o.length是有限数值
            o.length >= 0 &&                        // o.length为非负值
            o.length === Math.floor(o.length) &&    // o.length是整数
            o.length < 4294967296)                  // o.length < 2^32
            return true;
        else
            return false;
    }

打住!真的不再多啰嗦了,这篇文章篇幅不应这样的

call

丐版实现:

//传递参数从一个数组变成逐个传参了,不用...扩展运算符的也可以用arguments代替
Function.prototype.NealCall = function (context, ...args) {
    //这里默认不传就是给window,也可以用es6给参数设置默认参数
    context = context || window;
    args = args ? args : [];
    //给context新增一个独一无二的属性以免覆盖原有属性
    const key = Symbol();
    context[key] = this;
    //通过隐式绑定的方式调用函数
    const result = context[key](...args);
    //删除添加的属性
    delete context[key];
    //返回函数调用的返回值
    return result;
}

bind

bind的实现讲道理是比 apply 和call 麻烦一些的,也是面试频考题。因为需要去考虑函数的拷贝。但是也还是比较简单的,网上也有很多版本,这里就不具体展开了。具体的,咱们可以在群里讨论~

Function.prototype.myBind = function (objThis, ...params) {
    const thisFn = this; // 存储源函数以及上方的params(函数参数)
    // 对返回的函数 secondParams 二次传参
    let fToBind = function (...secondParams) {
        const isNew = this instanceof fToBind // this是否是fToBind的实例 也就是返回的fToBind是否通过new调用
        const context = isNew ? this : Object(objThis) // new调用就绑定到this上,否则就绑定到传入的objThis上
        return thisFn.call(context, ...params, ...secondParams); // 用call调用源函数绑定this的指向并传递参数,返回执行结果
    };
    if (thisFn.prototype) {
        // 复制源函数的prototype给fToBind 一些情况下函数没有prototype,比如箭头函数
        fToBind.prototype = Object.create(thisFn.prototype);
    }
    return fToBind; // 返回拷贝的函数
};
Function.prototype.myBind = function (context, ...args) {
    const fn = this
    args = args ? args : []
    return function newFn(...newFnArgs) {
        if (this instanceof newFn) {
            return new fn(...args, ...newFnArgs)
        }
        return fn.apply(context, [...args,...newFnArgs])
    }
}

最后

别忘记了上面 this 的考核题目啊,同学,该交卷了!

参考链接

Flutter 开发者国服最强辅助 App:FlutterGo 2.0 强势归来!!!

我们知道。。。这是我们对社区承诺的一个兑现!

前言

沉淀了数月,FlutterGo 终于迎来了第二次迭代更新!不仅新增个人中心的概念,还提供了第三方共建工具、并且,我们还完成了 FlutterGo web 版的开发。而这些~都在我们FlutterGo官网可见!!!

FlutterGo 2.0

本次更新内容

部分功能展示

  • FlutterGo App

  • FlutterGo 官网

  • FlutterGo Web 版

  • goCli 共建工具

共建计划

咳咳,敲黑板啦~~

Flutter 依旧再不断地更新,但仅凭我们几个 Flutter 爱好者在工作之余维护 FlutterGo 还是非常吃力的。所以这里,诚邀业界所有 Flutter 爱好者一起参与共建 FlutterGo!

此处再次感谢所有已经提交 pr 的小伙伴

共建说明

由于 Flutter 版本迭代速度较快,产生的内容较多, 而我们人力有限无法更加全面快速的支持Flutter Go的日常维护迭代, 如果您对flutter go的共建感兴趣, 欢迎您来参与本项目的共建.

凡是参与共建的成员. 我们会将您的头像与github个人地址收纳进我们的官方网站中.

共建方式

  1. 共建组件
  • 本次更新, 开放了 Widget 内容收录 的功能, 您需要通过 goCli 工具, 创建标准化组件,编写markdown代码。

  • 为了更好记录您的改动目的, 内容信息, 交流过程, 每一条PR都需要对应一条 Issue, 提交你发现的BUG或者想增加的新功能, 或者想要增加新的共建组件,

  • 首先选择你的issue在类型,然后通过 Pull Request 的形式将文章内容, api描述, 组件使用方法等加入进我们的Widget界面。

  1. 提交文章和修改bug
  • 您也可以将例如日常bug. 未来feature等的功能性PR, 申请提交到我们的的主仓库。

参与共建

关于如何提PR请先阅读以下文档

贡献指南

此项目遵循贡献者行为准则。参与此项目即表示您同意遵守其条款.

FlutterGo 期待你我共建~

具体 pr 细节和流程可参看 FlutterGo README 或 直接钉钉扫码入群

webView for Detail

前言

鉴于首页功能都已经完成了 80% 了,所以我们在这就先把detail页面写完了吧,这里detail页面我们主要采用webView的形式加载。

flutter_webview_plugin

同样的,我们在 flutter package 中搜索一个webView的package,这里,当然我们要选择“star”数最多的啦

webView

使用起来非常的简单,直接可以看 flutter_webView_plugin 的文档即可。

通过文档我们可以看到此webView具有的一些功能

  Future<Null> launch(String url,
           {Map<String, String> headers: null,
           bool withJavascript: true,
           bool clearCache: false,
           bool clearCookies: false,
           bool hidden: false,
           bool enableAppScheme: true,
           Rect rect: null,
           String userAgent: null,
           bool withZoom: false,
           bool withLocalStorage: true,
           bool scrollBar: true});

甚至类似Flutter中的Scaffold都是有的,这也正式我们需要的。

webView for detail

下面让我们重新编写我们的detail页面

首先在 pubspec.yaml 中注入我们需要的依赖这个不必多说。

  • lib/pages/article_detail.dart

      import 'package:flutter/material.dart';
      import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
      
      class ArticleDetail extends StatefulWidget {
        final String articleUrl;
        final String title;
        ArticleDetail(this.articleUrl, this.title);
        _ArticleDetailState createState() => _ArticleDetailState();
      }
      
      class _ArticleDetailState extends State<ArticleDetail> {
        bool hasLoaded = false;
        final flutterWebViewPlugin = new FlutterWebviewPlugin();
      
        @override
        void initState() {
          flutterWebViewPlugin.onStateChanged.listen((state) {
            if (state.type == WebViewState.finishLoad) {//有掘金web版本详情页的finished触发时间实在太长,所以这里就省略了hasLoaded的处理,其实也就是为了界面更友好
              setState(() {
                hasLoaded = true;
              });
            }
          });
          super.initState();
        }
      
        @override
        Widget build(BuildContext context) {
          return WebviewScaffold(
            url: widget.articleUrl,
            appBar: AppBar(
              title: Text(widget.title),
            ),
            withZoom: false,
            withLocalStorage: true,
            withJavascript: true,
          );
        }
      }
      

这里的代码说明下为什么是 StateFulWidget ,以为加载页面是需要时间的,所以我希望在加载的过程中有一个loading,而不是如果时间过长的话给用户的感觉是白板。当然,flutter_webView_plugin 也给我们提供了这方面的监听,但是!!! 咳咳,掘金的 detail 页面 貌似有些资源记载。。。恩,忒慢! 所以 WebViewState.finishLoad 触发非常非常非常慢,所以这里,我就没有用这个 state 去做什么事情了,但是这个口子,还是留下来了。 大家自己开发的时候还是需要的。

这一小节比较简单,现在我们的app就更有点模样出来啦。

2detail

更新

2018-12-05 Flutter更新到1.0后,flutter_webview_plugin会导致应用崩溃。目前需更新到^0.3.0+2版本

总结

截止目前,首页的功能编写就要告一段落了。 至此,你应该学会

  • flutter package 中查找需要的package
  • 学习如何使用package
  • webView的使用
  • article detail的编写

Flutter Go 代码开发规范 0.1.0 版

代码风格

标识符三种类型

大驼峰

类、枚举、typedef和类型参数

  class SliderMenu { ... }
  
  class HttpRequest { ... }
  
  typedef Predicate = bool Function<T>(T value);

包括用于元数据注释的类

  class Foo {
    const Foo([arg]);
  }
  
  @Foo(anArg)
  class A { ... }
  
  @Foo()
  class B { ... }

使用小写加下划线来命名库和源文件

  library peg_parser.source_scanner;
  
  import 'file_system.dart';
  import 'slider_menu.dart';

不推荐如下写法:

  library pegparser.SourceScanner;
  
  import 'file-system.dart';
  import 'SliderMenu.dart';

使用小写加下划线来命名导入前缀

  import 'dart:math' as math;
  import 'package:angular_components/angular_components'
      as angular_components;
  import 'package:js/js.dart' as js;

不推荐如下写法:

  import 'dart:math' as Math;
  import 'package:angular_components/angular_components'
      as angularComponents;
  import 'package:js/js.dart' as JS;

使用小驼峰法命名其他标识符

  var item;
  
  HttpRequest httpRequest;
  
  void align(bool clearItems) {
    // ...
  }

优先使用小驼峰法作为常量命名

  const pi = 3.14;
  const defaultTimeout = 1000;
  final urlScheme = RegExp('^([a-z]+):');
  
  class Dice {
    static final numberGenerator = Random();
  }

不推荐如下写法:

  const PI = 3.14;
  const DefaultTimeout = 1000;
  final URL_SCHEME = RegExp('^([a-z]+):');
  
  class Dice {
    static final NUMBER_GENERATOR = Random();
  }

不使用前缀字母

因为Dart可以告诉您声明的类型、范围、可变性和其他属性,所以没有理由将这些属性编码为标识符名称。

  defaultTimeout

不推荐如下写法:

  kDefaultTimeout

排序

为了使你的文件前言保持整洁,我们有规定的命令,指示应该出现在其中。每个“部分”应该用空行分隔。

在其他引入之前引入所需的dart库

  import 'dart:async';
  import 'dart:html';
  
  import 'package:bar/bar.dart';
  import 'package:foo/foo.dart';

在相对引入之前先引入在包中的库

  import 'package:bar/bar.dart';
  import 'package:foo/foo.dart';
  
  import 'util.dart';

第三方包的导入先于其他包

  import 'package:bar/bar.dart';
  import 'package:foo/foo.dart';
  
  import 'package:my_package/util.dart';

在所有导入之后,在单独的部分中指定导出

  import 'src/error.dart';
  import 'src/foo_bar.dart';
  
  export 'src/error.dart';

不推荐如下写法:

  import 'src/error.dart';
  export 'src/error.dart';
  import 'src/foo_bar.dart';

所有流控制结构,请使用大括号

这样做可以避免悬浮的else问题

  if (isWeekDay) {
    print('Bike to work!');
  } else {
    print('Go dancing or read a book!');
  }

例外

一个if语句没有else子句,其中整个if语句和then主体都适合一行。在这种情况下,如果你喜欢的话,你可以去掉大括号

  if (arg == null) return defaultValue;

如果流程体超出了一行需要分划请使用大括号:

  if (overflowChars != other.overflowChars) {
    return overflowChars < other.overflowChars;
  }

不推荐如下写法:

  if (overflowChars != other.overflowChars)
    return overflowChars < other.overflowChars;

注释

要像句子一样格式化

除非是区分大小写的标识符,否则第一个单词要大写。以句号结尾(或“!”或“?”)。对于所有的注释都是如此:doc注释、内联内容,甚至TODOs。即使是一个句子片段。

  greet(name) {
    // Assume we have a valid name.
    print('Hi, $name!');
  }

不推荐如下写法:

  greet(name) {
    /* Assume we have a valid name. */
    print('Hi, $name!');
  }

可以使用块注释(/…/)临时注释掉一段代码,但是所有其他注释都应该使用//

Doc注释

使用///文档注释来记录成员和类型。

使用doc注释而不是常规注释,可以让dartdoc找到并生成文档。

  /// The number of characters in this chunk when unsplit.
  int get length => ...

由于历史原因,达特茅斯学院支持道格评论的两种语法:///(“C#风格”)和/**…* /(“JavaDoc风格”)。我们更喜欢/// 因为它更紧凑。/*/在多行文档注释中添加两个无内容的行。在某些情况下,///语法也更容易阅读,例如文档注释包含使用*标记列表项的项目符号列表。

考虑为私有api编写文档注释

Doc注释并不仅仅针对库的公共API的外部使用者。它们还有助于理解从库的其他部分调用的私有成员

用一句话总结开始doc注释

以简短的、以用户为中心的描述开始你的文档注释,以句号结尾。

/// Deletes the file at [path] from the file system.
void delete(String path) {
  ...
}

不推荐如下写法:

  /// Depending on the state of the file system and the user's permissions,
  /// certain operations may or may not be possible. If there is no file at
  /// [path] or it can't be accessed, this function throws either [IOError]
  /// or [PermissionError], respectively. Otherwise, this deletes the file.
  void delete(String path) {
    ...
  }

“doc注释”的第一句话分隔成自己的段落

在第一个句子之后添加一个空行,把它分成自己的段落

  /// Deletes the file at [path].
  ///
  /// Throws an [IOError] if the file could not be found. Throws a
  /// [PermissionError] if the file is present but could not be deleted.
  void delete(String path) {
    ...
  }

Flutter_Go 使用参考

库的引用

flutter go 中,导入lib下文件库,统一指定包名,避免过多的../../

package:flutter_go/

字符串的使用

使用相邻字符串连接字符串文字

如果有两个字符串字面值(不是值,而是实际引用的字面值),则不需要使用+连接它们。就像在C和c++中,简单地把它们放在一起就能做到。这是创建一个长字符串很好的方法但是不适用于单独一行。

raiseAlarm(
    'ERROR: Parts of the spaceship are on fire. Other '
    'parts are overrun by martians. Unclear which are which.');

不推荐如下写法:

raiseAlarm('ERROR: Parts of the spaceship are on fire. Other ' +
    'parts are overrun by martians. Unclear which are which.');

优先使用模板字符串

'Hello, $name! You are ${year - birth} years old.';

在不需要的时候,避免使用花括号

  'Hi, $name!'
  "Wear your wildest $decade's outfit."

不推荐如下写法:

  'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';

不推荐如下写法:

  'Hi, ${name}!'
  "Wear your wildest ${decade}'s outfit."

集合

尽可能使用集合字面量

如果要创建一个不可增长的列表,或者其他一些自定义集合类型,那么无论如何,都要使用构造函数。

  var points = [];
  var addresses = {};
  var lines = <Lines>[];

不推荐如下写法:

  var points = List();
  var addresses = Map();

不要使用.length查看集合是否为空

if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');

不推荐如下写法:

  if (lunchBox.length == 0) return 'so hungry...';
  if (!words.isEmpty) return words.join(' ');

考虑使用高阶方法转换序列

如果有一个集合,并且希望从中生成一个新的修改后的集合,那么使用.map()、.where()和Iterable上的其他方便的方法通常更短,也更具有声明性

  var aquaticNames = animals
      .where((animal) => animal.isAquatic)
      .map((animal) => animal.name);

避免使用带有函数字面量的Iterable.forEach()

在Dart中,如果你想遍历一个序列,惯用的方法是使用循环。

for (var person in people) {
  ...
}

不推荐如下写法:

  people.forEach((person) {
    ...
  });

不要使用List.from(),除非打算更改结果的类型

给定一个迭代,有两种明显的方法可以生成包含相同元素的新列表

var copy1 = iterable.toList();
var copy2 = List.from(iterable);

明显的区别是第一个比较短。重要的区别是第一个保留了原始对象的类型参数

// Creates a List<int>:
var iterable = [1, 2, 3];

// Prints "List<int>":
print(iterable.toList().runtimeType);
// Creates a List<int>:
var iterable = [1, 2, 3];

// Prints "List<dynamic>":
print(List.from(iterable).runtimeType);

参数的使用

使用=将命名参数与其默认值分割开

由于遗留原因,Dart均允许“:”和“=”作为指定参数的默认值分隔符。为了与可选的位置参数保持一致,使用“=”。

  void insert(Object item, {int at = 0}) { ... }

不推荐如下写法:

  void insert(Object item, {int at: 0}) { ... }

不要使用显式默认值null

如果参数是可选的,但没有给它一个默认值,则语言隐式地使用null作为默认值,因此不需要编写它

void error([String message]) {
  stderr.write(message ?? '\n');
}

不推荐如下写法:

void error([String message = null]) {
  stderr.write(message ?? '\n');
}

变量

不要显式地将变量初始化为空

在Dart中,未显式初始化的变量或字段自动被初始化为null。不要多余赋值null

  int _nextId;
  
  class LazyId {
    int _id;
  
    int get id {
      if (_nextId == null) _nextId = 0;
      if (_id == null) _id = _nextId++;
  
      return _id;
    }
  }

不推荐如下写法:

  int _nextId = null;
  
  class LazyId {
    int _id = null;
  
    int get id {
      if (_nextId == null) _nextId = 0;
      if (_id == null) _id = _nextId++;
  
      return _id;
    }
  }

避免储存你能计算的东西

在设计类时,您通常希望将多个视图公开到相同的底层状态。通常你会看到在构造函数中计算所有视图的代码,然后存储它们:

应该避免的写法:

  class Circle {
    num radius;
    num area;
    num circumference;
  
    Circle(num radius)
        : radius = radius,
          area = pi * radius * radius,
          circumference = pi * 2.0 * radius;
  }

如上代码问题:

  • 浪费内存
  • 缓存的问题是无效——如何知道何时缓存过期需要重新计算?

推荐的写法如下:

  class Circle {
    num radius;
  
    Circle(this.radius);
  
    num get area => pi * radius * radius;
    num get circumference => pi * 2.0 * radius;
  }

类成员

不要把不必要地将字段包装在getter和setter中

不推荐如下写法:

  class Box {
    var _contents;
    get contents => _contents;
    set contents(value) {
      _contents = value;
    }
  }

优先使用final字段来创建只读属性

尤其对于 StatelessWidget

在不需要的时候不要用this

不推荐如下写法:

  class Box {
    var value;
    
    void clear() {
      this.update(null);
    }
    
    void update(value) {
      this.value = value;
    }
  }

推荐如下写法:

  class Box {
    var value;
  
    void clear() {
      update(null);
    }
  
    void update(value) {
      this.value = value;
    }
  }

构造函数

尽可能使用初始化的形式

不推荐如下写法:

  class Point {
    num x, y;
    Point(num x, num y) {
      this.x = x;
      this.y = y;
    }
  }

推荐如下写法:

class Point {
  num x, y;
  Point(this.x, this.y);
}

不要使用new

Dart2使new 关键字可选

推荐写法:

  Widget build(BuildContext context) {
    return Row(
      children: [
        RaisedButton(
          child: Text('Increment'),
        ),
        Text('Click!'),
      ],
    );
  }

不推荐如下写法:

  Widget build(BuildContext context) {
    return new Row(
      children: [
        new RaisedButton(
          child: new Text('Increment'),
        ),
        new Text('Click!'),
      ],
    );
  }

异步

优先使用async/await代替原始的futures

async/await语法提高了可读性,允许你在异步代码中使用所有Dart控制流结构。

  Future<int> countActivePlayers(String teamName) async {
    try {
      var team = await downloadTeam(teamName);
      if (team == null) return 0;
  
      var players = await team.roster;
      return players.where((player) => player.isActive).length;
    } catch (e) {
      log.error(e);
      return 0;
    }
  }

当异步没有任何用处时,不要使用它

如果可以在不改变函数行为的情况下省略异步,那么就这样做。、

  Future afterTwoThings(Future first, Future second) {
    return Future.wait([first, second]);
  }

不推荐写法:

  Future afterTwoThings(Future first, Future second) async {
    return Future.wait([first, second]);
  }

开源库、活动 UI & 功能 编写

前言

由于前面UI功能基本以及涵盖我们这一章节中所涵盖的知识点。所以活动和开源库的页面,我们将放到一小节中,将关键代码再熟悉一番。不再重复编写同样章节内容。完成该章节我们将完成如下功能开发:

img

代码地址为:代码地址

定义数据model

  • lib/model/activity_cell.dart
  import '../util/util.dart';
  class ActivityCell{
    String pic;
    String detailUrl;
    String title;
    String city;
    String time;
  
    ActivityCell({
      this.city,
      this.detailUrl,
      this.pic,
      this.time,
      this.title
    });
  
    factory ActivityCell.formJson(Map<String,dynamic> json){
      return ActivityCell(
        city: json['city'],
        detailUrl: json['eventUrl'],
        title:json['title'],
        pic:json['screenshot'],
        time: Util.getTimeDate(json['startTime'])
      );
    }
  }
  • 数据model的定义使我们每一个页面针对该页面的cell总结出来的所需字段,对于从接口获取的字段需要进行处理的数据也同样在这一层中处理,保证改model的数据都是cell可以拿来即用的。比如此处我们使用Util.getTimeDate的方法

具体方法如下:

  static String getTimeDate(String comTime) {
    var compareTime = DateTime.parse(comTime);
    String weekDay = '';
    switch (compareTime.weekday) {
      case 2:
        weekDay = '周二';
        break;
      case 3:
        weekDay = '周三';
        break;
      case 4:
        weekDay = '周四';
        break;
      case 5:
        weekDay = '周五';
        break;
      case 6:
        weekDay = '周六';
        break;
      case 7:
        weekDay = '周日';
        break;
      default:
        weekDay = '周一';
    }
    return '${compareTime.month}-${compareTime.day}  $weekDay';
  }

请求数据

首先定义基础请求api

  // 开源库
  static const String REPOS_LIST = 'https://repo-ms.juejin.im/v1/getCustomRepos';

  // 活动
  static const String ACTIVITY_CITY = 'https://event-storage-api-ms.juejin.im/v1/getCityList';
  static const String ACTIVITY_LIST = 'https://event-storage-api-ms.juejin.im/v2/getEventList';

再在lib/util/data_utils.dart中封装请求方法,这里也是举其一个例子

  // 活动列表
    static Future<List<ActivityCell>> getActivityList(Map<String,dynamic> params) async{
    List<ActivityCell> resultList = [];
    var response = await NetUtils.get(Api.ACTIVITY_LIST,params: params);
    var responseList = response['d'];
    for (int i = 0; i < responseList.length; i++) {
      ActivityCell activityCell;
      try {
        activityCell = ActivityCell.formJson(responseList[i]);
      } catch (e) {
        print("error $e at $i");
        continue;
      }
      resultList.add(activityCell);
    }
     return resultList;
  }
  • 由于我不确定字段是否齐全,毕竟这是掘金开放的api,笔者也没有跟开发约束字段,所以在处理的字段的时候添加了个异常捕获。及时出现了异常,也不会影响代码,并且还能定位哪一个数据有问题。

页面代码编写

这是我们改项目中编写的最后一个完整的页面,所以这里我展示下全部的代码,再最后说明下其中的一些注意事项

  • lib/page/repos_page.dart

        import 'package:flutter/material.dart';
        import '../util/data_utils.dart';
        import '../model/repos_cell.dart';
        import '../constants/constants.dart';
        import '../widgets/repos_list_cell.dart';
        import '../widgets/load_more.dart';
        import 'dart:core';
        import '../widgets/repos_cell_header.dart';
        
        class ReposPage extends StatefulWidget {
          _ReposPageState createState() => _ReposPageState();
        }
        
        class _ReposPageState extends State<ReposPage> {
          List<ReposCell> _listData = <ReposCell>[];
          int _indexPage = 0;
          Map<String, dynamic> _params = {"src": 'web', "limit": 20};
          bool _hasMore = true;
          ScrollController _scrollController = ScrollController();
           bool _isRequesting = false;
        
          @override
          void initState() {
            super.initState();
            _getListData(false);
            _scrollController.addListener(() {
              if (_scrollController.position.pixels ==
                  _scrollController.position.maxScrollExtent) {
                _getListData(true);
              }
            });
          }
      
        _getListData(bool isLoadMore) {
          if (_isRequesting || !_hasMore) return;
          if (isLoadMore) {
            _params['before'] = Constants.REPOS_BEFOR[_indexPage];
          }else{
            _indexPage = -1;
          }
          _isRequesting = true;
          DataUtils.getReposListData(_params).then((resultData) {
            if (this.mounted) {
              _indexPage+=1;
              List<ReposCell> resultList = [];
              if (isLoadMore) {
                resultList.addAll(_listData);
              }
              resultList.addAll(resultData);
      
              setState(() {
                _listData = resultList;
                _hasMore = _indexPage < Constants.REPOS_BEFOR.length;
                _isRequesting = false;
              });
            }
          });
        }
      
          @override
          void dispose() { 
            _scrollController.dispose();
            super.dispose();
          }
        
          Widget _itemBuilder(context,index){
            if(index == _listData.length+1){
              return LoadMore(_hasMore);
            }
            if(index == 0){
              return ReposCellHeader();
            }
            return ReposListCell(cellData: _listData[index-1]);
          }
        
          @override
          Widget build(BuildContext context) {
            return ListView.builder(
              itemBuilder: _itemBuilder,
              itemCount: _listData.length+2,
              controller: _scrollController,
            );
          }
        }

  • 顶部import相关组件,设计该页面的是repos_cell_header,repos_list_cell,data_util为页面请求数据封装的方法。load_more 是底部加载更多的组件,需要传递hasMore来确认UI长什么样子。dart:core为dart中的核心库,包含Uri的加密解密等
  • 页面继承StatefulWidget类,因为会涉及到页面UI的改变,请求数据我们需要在内部定义盛放数据的list,pageIndex、请求参数、是否还有下一页、是否正在请求(防止触底后不断发送页面请求)
  • 长列表使用ListViedw.builder方法来构建,毕竟性能好,构建需要初始化 ScrollController ,ScrollController可以给列表设置长度,并且还可以检测页面滚动,检测触底,在页面初始化的时候添加这些监听。
  • 在请求数据的时候,需要根据页面当前的状态(isRequesting、hasMore、isLoadMore)来决定进行什么操作
  • 最后由于我们初始化 ScrollController ,所以需要在页面 dispose的时候及时释放相应资源,以保证手机性能

完整代码地址:代码地址

记一个复杂组件(Filter)的从设计到开发

此文前端框架使用 rax,全篇代码暂未开源(待开源)

前言

貌似在面试中,你如果设计一个 react/vue 组件,貌似已经是司空见惯的问题了。本文不是理论片,更多的是自己的一步步思考和实践。文中会有很多笔者的思考过程,欢迎评论区多多交流和讨论。

从需求讨论、技术方案探讨到编码、到最终的测试,经历过了很多次的脑暴,也遇到过非常多的坑,其中有可能跟业务有关、也有可能跟框架有关,基于这些坑,又讨论了很多解决方案和非常 hack(歪门邪道)的对策。但是随着时间的推移,再回头看看当时的 hack 代码,很多都不太记得为什么这么写了,所以这里简单记录下,Filter 组件的开发过程。以便后面查询,更希望能大家一起探讨,以求得更优质的代码架构和实现思路。

由于代码编写使用基于底层 weex 的 rax 框架,所以有些坑,或许对于正在使用 react 或者 vue 的你并不会遇到,可以直接忽略

说说业务

Filter,已经常见的不可再常见的组件了,顾名思义,就是个筛选过滤器。我们先看看现有 app 上的一些 filter 展现 形式。既然做组件,我们就需要它足够的通用,足够的易于扩展。

  • 阿里拍卖的 Filter

paimai

  • 飞猪的 Filter

feizhu

在说 Filter 的业务特征之前,我们先约束下每一部分的命名,以便于你更好的阅读此文:

IMAGE

上面分别是拍卖和飞猪的 filter 页面,从这两个页面中,我们大概可以总结出关于 Filter 的一下几点业务画像:

  • 随着页面滚动,Filter 可能具有吸附能力,但是可能距离顶部存在一定的距离
  • Panel 面板多样性(点击navItem 展开的面板)
  • Panel 面板以及 navItem 都可能会有动画
  • navBar 内容可变
  • panel 面板展示形式不定
  • panel 面板内容可能非常复杂,需要考虑性能优化
  • navBar 上可能存在非 Filter 的内容(关注按钮)
  • 有的navBar 的 navItem 没有对应的 panel 面板
  • Filter 上存在影响搜索结果但是没有影响的”快排“按钮
  • filter 配置参数能够指定
  • 通过 url 传入相关筛选 id 能够初始化面板选中
  • ...

最终组件产出

由于 rax 1.0 ts+hooks 开源版本还在开发中,所以仓库链接暂时就不放上了

  • rax-pui-filter-utils : Filter 的内部工具库,仅供 Filter 开发者提供的工具库
  • rax-pui-filter-tools:配合使用 Filter 的一些工具集,比如 提高性能的 HOC 组件、占位符组件等(可用可不用,根据自己业务需求来),思考原由:并不是每一个 Filter 的使用者都需要这些功能,做成可插拔式,为了降低没必要的 bundle 大小
  • pui-filter:Filter 核心功能开发库

效果图:

console 处可见抛出的查询参数

设计与思考

前端组件架构图(初版)

组件架构图(终板)

src
├─ Filter.js    //Filter 最外层父容器
├─ constant.js  //项目代码常量定义
├─ index.js     //入口文件
├─ navbar       // navBar 文件夹
│    ├─ NavBase.js    //navBar 基类 NavQuickSearch 和 NavRelatePanel 父类
│    ├─ NavQuickSearch.js   // 快速搜索(无 panel)的 navBar
│    ├─ NavRelatePanel.js   // 带有 panel 的 navBar
│    └─ index.js  // 导出文件
├─ panel
│    └─ index.js  // panel 面板组件代码
└─ style.js

组件功能 Feature

  • 筛选头 UI 可动态配置扩展,支持点击动画,提供三种筛选项类型
    • RelatePanel筛选项关联Panel型,即筛选头和 Panel 是一对一关系,点击筛选头展示 Panel
    • QuickSearch筛选项快速搜索排序型,即筛选头没有对应 Panel,点击筛选头直接触发搜索
    • PureUI纯 UI占位类型,即纯 UI 放置,不涉及搜索,比如订阅按钮场景
  • 筛选面板显示隐藏统一管理,支持下拉和左滑展示隐藏动画,统一搜索回调函数
  • Filter 组件在和业务面板隔离,支持任意组件接入,业务组件里搜索变更通过 onChange(params)回调函数来触发
  • 提供了三种业务通用的面板组件
    • rax-pui-list-select,列表选择业务面板
    • rax-pui-location-select,省市区级联选择业务面板
    • rax-pui-multi-selection-panel,多选业务面板,查看组件使用文档

这里指的是 Filter 的功能 Feature,跟上文提及的 Filter 组件功能可能并不能完全覆盖,但是我们提供解决方案,组件的设计始终秉持着不侵入业务的原则,所有与业务相关均给予配置入口。

期望组件使用形式

 import Filter from 'rax-pui-filter';

  render(
    <Filter
    navConfig={[]}
    onChange={()=>{}}>
      <Filter.Panel>
          <业务组件1 />
      </Filter.Panel>
      <Filter.Panel>
          <业务组件2 />
      </Filter.Panel>
    </Filter>
  );
  

组件功能与业务需求边界划分

何为业务功能何为组件功能,这个需要具体的探讨,其实也没有严格意义上的区分。说白了,就是你买个手机,他都会送你充电器。但是。。。为什么很多手机也送手机壳(小米、华为、荣耀)但是 iPhone 却不送呢?所以到底是不是标配?

对于我们这个组件,简而言之:我们能做到的,我们都做!但是其中我们还是梳理出某些功能还是数据业务功能:

  • navBar 上每一个 navItem 展示什么文案、样式属于业务功能
  • 整个 Filter 的数据处理,包括 url 上的查询参数需要抛给对应 navItem要展示的文案也是业务功能
  • Filter 是否点击滚动到顶部也是业务功能,毕竟很多搜索页 Filter 本身置顶。而且,对于 rax 而言,不同容器滚动方式还不同(但是我们提供这样的方法给你去调用)
  • panel 面板里面数据请求、逻辑处理都是你自己的业务逻辑。Filter 只提供基本的容器能力和接口

换言之,Filter 里面任何功能都可以说为业务功能。但是我们需要提供 80%业务都需要的功能封装作为 Filter 的 Future。这就是我们的目的。

根据上面的业务功能和组件功能的区分,我们就知道在使用 Filter 的时候,你应该给我传递什么配置,以及什么方法。

Filter API

参数 说明 类型 默认值(是否必填)
navConfig 筛选头配置, 点击查看详细配置项

效果图
undefined
Array<Object> - (必填)
offsetTop Filter组件展开面板状态下距离页面顶部的高度,有两种状态:固定位置跟随页面滚动吸附置顶

固定位置 状态下距离页面顶部的高度
跟随页面滚动吸附置顶: 状态下距离页面顶部的高度

效果图
undefined
Number 0
styles 配置样式,Filter中所有样式都可使用styles集合对象来配置覆盖
styles 格式
undefined
Object {}
getStickyRef 获取 Sticky 节点的 ref 实例,用于滚动吸附场景,内部配合 pm-app-plus 容器组件点击 Filter 时自动吸附置顶

示例图
undefined
Function
keepHighlight 筛选条件改变后是否需要在筛选头保持高亮

效果图
undefined
Boolean false
clickMaskClosable 开启 mask 背景的点击隐藏 Boolean true
onChange Filter 搜索变更回调函数
签名: Function(params:Object,index:Number, urlQuery: Object) => void
参数:
params: Object 搜索参数
index:Number 触发搜索的 Panel 搜索
urlQuery:Object URL query 对象
Function
onPanelVisibleChange Panel 显示隐藏回调函数
签名: Function({ visible:Boolean, triggerIndex:Number, triggerType:String }) => void
参数:
visible:Boolean 显示隐藏标志量
triggerIndex:Number触发的筛选项索引值
triggerType:String 触发类型


triggerType详解 包含三种触发类型
Navbar:来自筛选头的点击触发
Mask:来自背景层的点击触发
Panel:来自Panel 的 onChange 回调触发
Function

Filter prop navConfig 数组配置详解

navConfig

筛选项类型 type

  • RelatePanel筛选项关联Panel型,即筛选头和 Panel 是一对一关系,点击筛选头展示 Panel
  • QuickSearch筛选项快速搜索排序型,即筛选头没有对应 Panel,点击筛选头直接触发搜索
  • PureUI纯 UI占位类型,即纯 UI 放置,不涉及搜索,比如订阅按钮场景

注意 如果 navConfig 内置的UI参数不满足您的需求,请使用renderItem自定义渲染函数来控制筛选头 UI

参数 说明 类型 默认值(是否必填)
type 筛选项类型

三种类型
RelatePanel: 筛选项关联数据面板类型
QuickSearch: 筛选项快速搜索排序类型
PureUI: 纯 UI占位类型
String 'RelatePanel'
text


注意 RelatePanel类型生效
筛选头显示文案
文字溢出用...展示
String - (必填)
icons


注意 RelatePanel类型生效
筛选头 icon:normal 正常态 和 active 激活态 图标
数据格式
Object类型 :
undefined
String类型 :
undefined

效果图
undefined
Object or String -
options


注意 QuickSearch类型生效
快速搜索排序类型的数据源
数据格式
undefined
Array (必填)
optionsIndex


注意 QuickSearch类型生效
快速搜索排序类型默认选中的索引 String 0
optionsKey


注意 QuickSearch类型生效
指定快速搜索排序对应的搜索 key,用到 onChange 回调中 String 不提供默认使用当前筛选项的索引
formatText 文案格式化函数
签名:Function(text:String) => text
参数:
text: String 筛选头文案
Function (text)=>text
disabled 禁用筛选头点击 Boolean true
hasSeperator 是否展示右侧分隔符

效果图
undefined
Boolean false
hasPanel 当前筛选头是否有对应的 panel Boolean true
renderItem 自定义渲染
注意
提供的配置项无法满足你的 UI 需求时使用
签名:Function(isActive:Boolean, this:Element) => Element
参数:
isActive:Boolean 筛选头是否为激活状态
this:Element 筛选头this实例
Function -
animation 动画配置,采用内置的动画
参数说明
undefined
注意 目前只内置了一种rotate动画类型
Object
animationHook 用户自定义动画的钩子函数,内置动画无法满足需求时使用
签名:Function(refImg:Element, isActive:Boolean) => text
参数:
refImg:Element 筛选头图标的 ref 实例
isActive:Boolean 筛选头是否为激活状态
Function -

Filter.Panel API

参数 说明 类型 默认值(是否必填)
styles 配置样式
Filter中所有样式都可使用styles集合对象来配置覆盖
Object {}
displayMode Panel 展现形式:全屏、下拉
参数说明
全屏:Fullscreen
下拉:Dropdown
String 'Dropdown'
noAnimation 禁止动画 Boolean true
highPerformance 内部通过 Panel 的显示隐藏控制 panel 的 render 次数,避免不必要的 render,高性能模式下,只会在 Panel 展示 或者 展示隐藏状态变化时才会重新 render Boolean true
animation Panel 展示动画配置,内置上下左右动画
参数说明
undefined
direction 控制动画方向,分别有 updownleftright
Object

Filter 的代码使用

  • Filter 的参数配置
  navConfig: [
        {
          type: 'RelatePanel', // type可以不提供,默认值为'RelatePanel'
          text: '向下', // 配置筛选头文案
          icons: {
            // 配置 icon,分为正常形态和点击选中形态
            normal: '//gw.alicdn.com/tfs/TB1a7BSeY9YBuNjy0FgXXcxcXXa-27-30.png',
            active: '//gw.alicdn.com/tfs/TB1NDpme9CWBuNjy0FhXXb6EVXa-27-30.png',
          },
          hasSeperator: true, // 展示竖线分隔符
          formatText: text => text + '↓', // 筛选文案的格式化函数
        },
        {
          type: 'QuickSearch',
          optionsIndex: 0,
          optionsKey: 'price',
          options: [
            // 快速排序列表
            {
              text: '价格',
              icon: '',
              value: '0',
            },
            {
              text: '升序',
              icon: '//gw.alicdn.com/tfs/TB1PuVHXeL2gK0jSZFmXXc7iXXa-20-20.png',
              value: '1',
            },
            {
              text: '降序',
              icon: '//gw.alicdn.com/tfs/TB1a7BSeY9YBuNjy0FgXXcxcXXa-27-30.png',
              value: '2',
            },
          ],
        },
        {
          type: 'RelatePanel', // type可以不提供,默认值为'RelatePanel'
          text: '旋转',
          icons: {
            // 配置 icon,分为正常形态和点击选中形态
            normal: '//gw.alicdn.com/tfs/TB1PuVHXeL2gK0jSZFmXXc7iXXa-20-20.png',
            active: '//gw.alicdn.com/tfs/TB1l4lIXhv1gK0jSZFFXXb0sXXa-20-20.png',
          },
          animation: { type: 'rotate' }, // 配置动画点击后旋转图片,默认没有动画
        },
        {
          type: 'RelatePanel', // type可以不提供,默认值为'RelatePanel'
          text: '向左',
        },
        {
          type: 'PureUI',
          text: '订阅',
          renderItem: () => {
            // 渲染自定义的 UI
            return (
              <Image
                style={{
                  width: 120,
                  height: 92,
                }}
                source={{ uri: 'https://gw.alicdn.com/tfs/TB1eubQakL0gK0jSZFAXXcA9pXa-60-45.png' }}
              />
            );
          },
        },
      ]
      
      
      // ...
      
        <Filter
              offsetTop={100} // offsetTop = RecycleView上面的组件的高度,当前为 100
              navConfig={this.state.navConfig} // Filter Navbar 配置项
              keepHighlight={true} // 保持变更的高亮
              styles={styles} // 配置覆盖内置样式,大样式对象集合
              onChange={this.handleSearchChange}
              // Panel 面板显示隐藏变更事件
              onPanelVisibleChange={this.handlePanelVisibleChange}>
              <Panel highPerformance={true}>
                <ListSelect {...this.state.data1} />
              </Panel>
              <Panel>
                <LocationSelect {...this.state.data2} />
              </Panel>
              <Panel
                displayMode={'Fullscreen'} // 配置 Panel 全屏展示,默认为下拉展示
                animation={{
                  // 动画配置
                  timingFunction: 'cubic-bezier(0.22, 0.61, 0.36, 1)',
                  duration: 200,
                  direction: 'left', // 动画方向:从右往左方向滑出
                }}>
                <MultiSelect {...this.state.data3} />
              </Panel>
            </Filter>

代码运行效果图如上截图。下面,简单说下代码的实现。

核心源码展示

开源版本(Ts+hooks+lerna)还未公布,所以目前还是采用 rax 0.x 的版本编写的代码。这里只做,有坑的地方代码处理讲解。欢迎各位大佬评论留出各位想法

Filter.js

先从 render 方法看起

  render() {
    const { style = {}, styles = {}, navConfig, keepHighlight } = this.props;
    const { windowHeight, activeIndex } = this.state;
    if (!windowHeight) return null;

    return (
      <View style={[defaultStyle.container, styles.container, style]}>
        {this.renderPanels()}
        <Navbar
          ref={r => {
            this.refNavbar = r;
          }}
          navConfig={navConfig}
          styles={styles}
          keepHighlight={keepHighlight}
          activeIndex={activeIndex}
          onNavbarPress={this.handleNavbarPress}
          onChange={this.handleSearchChange}
        />
      </View>
    );
  }

获取一些基本配置,以及 windowHeight(屏幕高度)和 activeIndex(当前第几个item 处于 active 状态(被点开))。

之所以我们的 renderPanels 写在 NavBar 上面,是因为在 weex 中,zIndex 是不生效的。若想 A 元素在 B 元素上面,则 render 的时候,A 必须在 B 后面。这样写是为了 panel 面板展开的下拉动画,看起来是从 navBar 下面出来的。

renderPanel 方法就是渲染对应的 panel

  /**
   * 渲染 Panel
   */
  renderPanels = () => {
    const { activeIndex, windowHeight } = this.state;
    let { children } = this.props;

    if (!Array.isArray(children)) {
      children = [children];
    }

    let index = 0;
    return children.map(child => {
      let panelChild = null;
      let hasPanel = this.panelIndexes[index];
      if (!hasPanel) {
        index++;
      }
      if (!this.panelManager[index]) {
        this.panelManager[index] = {};
      }
      let injectProps = {
        index,
        visible: activeIndex === index,
        windowHeight,
        filterBarHeight: this.filterBarHeight,
        maxHeight: this.filterPanelMaxHeight,
        shouldInitialRender: this.panelManager[index].shouldInitialRender,
        onChange: this.handleSearchChange.bind(this, index),
        onNavTextChange: this.handleNavTextChange.bind(this, index),
        onHidePanel: this.setPanelVisible.bind(this, false, index),
        onMaskClick: this.handleMaskClick,
        disableNavbarClick: this.disableNavbarClick,
      };
      if (child.type !== Panel) {
        panelChild = <Panel {...injectProps}>{child}</Panel>;
      } else {
        panelChild = cloneElement(child, injectProps);
      }
      index++;
      return panelChild;
    });
  };

准确的说,这是一个 HOC,我们将代理、翻译传给 Filter 的影响或者 panel 面板需要使用的 props 传递给 Panel 面板。比如 onChange 回调,或者面板隐藏的回调以及当前哪一个 panel 需要展开等。

由于 Panel 的面板复杂度我们未知。为了避免不断的展开和收齐不必要的 render,我们采用 transform的方式,将面板不需要显示的面板移除屏幕外,需要展示的在移入到屏幕内部。具体可见 Panel 的render return

  return (
      <View
        ref={r => {
          this.refPanelContainer = r;
        }}
        style={[
          defaultStyle.panel,
          styles.panel,
          this.panelContainerStyle,
          {
            transform: `translateX(-${this.containerTransformDes})`,
            opacity: 0,
          },
        ]}>
        <View
          ref="mask"
          style={[
            defaultStyle.mask,
            styles.mask,
            showStyle,
            isWeb ? { top: 0, zIndex: -1 } : { top: 0 },
          ]}
          onClick={this.handleMaskClick}
          onTouchMove={this.handleMaskTouchMove}
        />
        {cloneElement(child, injectProps)}
      </View>
    );

注意: Panel 面板的坑远不止这些,比如,我们都知道,render 是最消耗页面性能的,而页面初始化进来,面板名没有展示出来(此时面板 Panel 在屏幕外),那么是否需要走 Panel 面板的 render 呢?但是目前的这种写法,Panel 组件的生命周期是会都走到的。但是如果遇到 Panel 里面需要请求数据,然后页面 url 里查询参数有 locationId=123 ,navItem 需要展示对应的地理位置.如果不渲染 Panel 如何根据 id 拿到对应的地名传递给 navItem 去展示?对,我们可以拦截 Panel 面板的 render 方法,让 Panel render null,然后别的生命周期照样运行。但是,如果 render 中用户有对 ref 的使用,那么就可能会造成难以排查的 bug。

所以最终,为了提高页面的可交互率但是又不影响页面需求的情况下,我们提供了一个可选的工具:Performance HOC 。 注意,是可选。

export default function performance(Comp) {
  return class Performance extends Comp {
    static displayName = `Performance(${Comp.displayName})`;
    render() {
      const { shouldInitialRender } = this.props.panelAttributes;
      if (shouldInitialRender) {
        return super.render();
      } else {
        return <View />;
      }
    }
  };
}

通过配置Panel 的 shouldInitialRender 属性来告诉我,是否第一次进来,拦截 render。

当然,Panel 也有很多别的坑,比如,现在 Panel 为了重复 render,将 Panel 移除屏幕外,那么,动画从上而下展开设置初始动画闪屏如何处理?

Filter 的代码就是初始化、format、检查校验各种传参,以及 Panel 和 NavBar 通信中转 比如 format、比如 handleNavbarPress

NavBar 核心代码

NavBar 架构

核心代码

从架构图中大概可以看出,NavBar 中通过不同的配置,展示不同的 NavBarItem 的类型,NavQuickSearch,NavRelatePanel

这里需要注意的是: NavBar 的数据是通过 Filter props 传入的,如果状态放到 Filter 也就是 NavBar 的父组件管理的话,会导致 Panel 组件不必要的渲染(虽然已经提供 Panel 层的 shouldComponentUpdate 的配置参数),同时也是为了组件设计的高内聚、低耦合,我们将传入的 props 封装到 NavBar 的 state 中,自己管理状态。

  constructor(props) {
    super(props);
    const navConfig = formatNavConfig(props.navConfig);

    this.state = {
      navConfig,
    };
  }
  // 这里我们提供内部的 formatNavConfig 方法,具体内容根据不同组件业务需求不同代码逻辑不同,这里就不展开说明了
  

NavBar 中还需要注意的就是被动更新:Panel 层点击后,NavBar 上文字的更新,因为这里我们利用父组件来进行 Panel 和 NavBar 的通信

  //Filter.js 调用 NavBar 的方法
  
  /**
   * 更新 Navbar 文案
   */
  handleNavTextChange = (index, navText, isChange = true) => {
    // Navbar 的 render 抽离到内部处理,可以减少一次 Filter.Panel 的额外 render
    this.asyncTask(() => {
      this.refNavbar.updateOptions(index, navText, isChange);
    });
  };
  
  //NavBar.js 提供给 Filter.js 调用的 updateOptions
  
    /**
   * 更新 navConfig,Filter 组件调用
   * 异步 setState 规避 rax 框架 bug: 用户在 componentDidMount 函数中调用中 this.props.onChange 回调
   * 重现Code:https://jsplayground.taobao.org/raxplayground/cefec50a-dfe5-4e77-a29a-af2bbfcfcda3
   * @param index
   * @param text
   * @param isChange
   */
  updateOptions = (index, text, isChange = true) => {
    setTimeout(() => {
      const { navConfig } = this.state;
      this.setState({
        navConfig: navConfig.map((item, i) => {
          if (index === i) {
            return {
              ...item,
              text,
              isChange,
            };
          }
          return item;
        }),
      });
    }, 0);
  };

最后 NavBar 中的 item 分为 快速搜索和带有 panel 的 NavBarItem两种,但是对于其公共功能,比如渲染的 UI 逻辑等,这里我们采用的方法是抽离 NavBase 组件,供给 NavQuickSearchNavRelatePanel 调用:

  • NavBase 部分代码
  renderDefaultItem = ({ text, icons, active }) => {
    const { formatText, hasSeperator, length, keepHighlight, isChange } = this.props;

    const hasChange = keepHighlight && isChange;
    const iconWidth = icons ? this.getStyle('navIcon').width || 18 : 0;

    return [
      <Text
        numberOfLines={1}
        style={[
          this.getStyle('navText'),
          ifElse(active || hasChange, this.getStyle('activeNavText')),
          { maxWidth: 750 / length - iconWidth },
        ]}>
        {ifElse(is('Function')(formatText), formatText(text), text)}
      </Text>,
      ifElse(
        icons,
        <Image
          ref={r => {
            this.refImg = r;
          }}
          style={this.getStyle('navIcon')}
          source={{
            uri: ifElse(active || hasChange, icons && icons.active, icons && icons.normal),
          }}
        />,
        null,
      ),
      ifElse(hasSeperator, <View style={this.navSeperatorStyle} />),
    ];
  };
  • NavRelatePanel.js
  export default class NavRelatePanel extends NavBase {
    static displayName = 'NavRelatePanel';
  
    handleClick = () => {
      const { disabled, onNavbarPress } = this.props;
      if (disabled) return false;
      onNavbarPress(NAV_TYPE.RelatePanel);
    };
  
    render() {
      const { renderItem, active, text, icons } = this.props;
  
      return (
        <View
          style={[this.getStyle('navItem'), ifElse(active, this.getStyle('activeNavItem'))]}
          onClick={this.handleClick}>
          {ifElse(
            is('Function')(renderItem),
            renderItem && renderItem({ active, instance: this }),
            this.renderDefaultItem({ text, icons, active }),
          )}
        </View>
      );
    }
  }

Panel 核心代码

Panel 的核心功能是对用户定义的 Panel.child 进行基本的功能添加,比如背景 mask 遮罩、动画时机的处理.

Panel 的使用:

              <Panel
                displayMode={'Fullscreen'} // 配置 Panel 全屏展示,默认为下拉展示
                animation={{
                  // 动画配置
                  timingFunction: 'cubic-bezier(0.22, 0.61, 0.36, 1)',
                  duration: 200,
                  direction: 'left', // 动画方向:从右往左方向滑出
                }}>
                <MultiSelect {...this.state.data3} />
              </Panel>

我们提供基础的动画配置,但是同时,也提供动画的 functionHook,这些都取决于动画的触发时机


  get animationConfig() {
    const { animation } = this.props;
    if (!animation || !is('Object')(animation)) {
      return PANEL_ANIMATION_CONFIG;
    }
    return Object.assign({}, PANEL_ANIMATION_CONFIG, animation);
  }
  
  // ... 
  
  
  /**
   * 执行动画
   * @param nextProps
   */
  componentWillReceiveProps(nextProps) {
    if (nextProps.visible !== this.props.visible) {
      if (nextProps.visible) {
        setNativeProps(findDOMNode(this.refPanelContainer), {
          style: {
            transform: `translateX(-${rem2px(750)})`,
          },
        });
        this.props.disableNavbarClick(true);
        this.enterAnimate(this.currentChildref, () => {
          this.props.disableNavbarClick(false);
        });
        this.handleMaskAnimate(true);
      } else {
        this.handleMaskAnimate(false);
        this.props.disableNavbarClick(true);
        this.leaveAnimate(this.currentChildref, () => {
          this.props.disableNavbarClick(false);
          setNativeProps(findDOMNode(this.refPanelContainer), {
            style: {
              transform: 'translateX(0)',
            },
          });
        });
      }
    }
  }

由于动画的执行需要时间,所以这个时间段,我们应该给 Filter 中的 NavBar 加锁 ,锁的概念也同样提供给用户,毕竟业务逻辑我们是不会侵入的,在上一次的搜索没有结果返回时候,应该给 NavBar 加锁,禁止再次点击(虽然用户可以再 onchange 回调函数中处理,但是作为组件,同样应该考虑并且提供这个能力),同样对于动画也是如此,在该动画正在执行的时候,应该禁止 NavBar 的再次点击。上面的动画配置效果如下:

Panel 中还有核心的处理或许就是关于动画时机的处理。比如在触发动画前,我们需要设置动画初始状态,但是如若如下写法,会出现 Panel 闪动的现象,毕竟我们通过第二次的事件轮训回来才执行初始化,所以这里,如果用户配置启动动画,那么我们需要在 Panel 的最外层添加一个可见的 flag:默认进来 opacity 设置为 0,当动画初始状态设置完毕后,在将最外层容器的 opacity 设置为 1,其实 Panel 还是闪了一下,只是你看不到而已。

      // 设置动画初始样式
      setTimeout(() => {
        setNativeProps(node, {
          style: {
            transform: !visible ? 'translate(0, 0)' : v,
          },
        });
      }, 0);
      // 执行动画
      setTimeout(() => {
        transition(
          node,
          {
            transform: visible ? 'translate(0, 0)' : v,
          },
          {
            timingFunction: timingFunction,
            duration: duration,
            delay: 0,
          },
          cb,
        );
      }, 50);

设置动画初始化样式中添加:

        setNativeProps(findDOMNode(this.refPanelContainer), {
          style: {
            opacity: 1,
          },
        });

结束语

Filter 的组件看似简单,但是如果想写一个市场上较为通用和广泛的 Filter 组件,不仅仅是组件的颗粒度、耦合度和性能需要考虑,更多的是其中还是有太多的业务逻辑需要去思考。对于目前的初版(还未修改成正式开源版),已经基本涵盖了目前我们能够想到的业务场景,也已经有相关业务落地使用。

当然,对于如果是直接放到业务中使用而不作为开源组件的话,我们可已经 Panel下的 child 通过 renderPortal 降低层级,通过 EventBus 或者 redux、mobx 等管理数据状态。那样会让整个代码逻辑看起来清晰很多。但是为了降低bundle 大小,我们尽可能的减少通用包的使用以及第三方插件的依赖。

关于文章中没有提及的想法或者对于这些Filter业务需求(坑)你有更好的处理方法和想法都欢迎在评论区交流~

技术交流

欢迎关注微信公众号:全栈前端精选,每日获取高质量文章推送。也可以加我个人微信交流~

盘一盘Flutter中“小巧”的组件与属性

前言

不仅仅局限于 widget,可能会从widget中衍生出一些相似的 package

鉴于市面上关于 Flutter 常用widget介绍已经非常多了,这里主要介绍下那些不常用,但是也非常实用的一些widget。

原文地址:Nealyang/personalBlog

demo地址:Nealyang/flutter

Widgets && property

Opacity

Opactiy 透明度,跟css的属性非常相似,连带child也同样透明,当然,其实Opacity本身是一个widget,如果不添加child也就没有透明的意义。

Opacity控制子 Widget 的透明度,opacity 属性为必传,可以设置 0.0~1.0之间的值。

          Opacity(
            opacity: .5,
            child: Container(
              height: 30.0,
              color: Colors.blue,
              child: Center(child:Text('透明度为:0.5')),
            ),
          ),
          Opacity(
            opacity: 1.0,
            child: Container(
              height: 30.0,
              color: Colors.red,
              child: Center(child:Text('透明度为:1.0')),
            ),
          ),

img

FadeInImage

带有淡入动画的 Image 组件,placeHodler是image到目标图片的一种过渡widget。使用FadeInImage可以类似网络加载的图片以一个更加优雅的形式出现在屏幕上,如果这个图片已经被加载了,或者已经存在内存中,那么placeholder图片将不会显示。

FadeInImage的写法跟Image类似,有很多别的命名构造函数。fadeOutDuration和fadeOutCurve控制placeholder的淡出动画。fadeInDuration和fadeInCurve控制目标图像的淡入动画,对于placeholder,更倾向于使用已经缓存的,以防止他也会突然的出现在屏幕上

          FadeInImage.assetNetwork(
            placeholder: 'assets/images/tst.jpg',
            image: "https://img.alicdn.com/tfs/TB13Xh3BkvoK1RjSZFNXXcxMVXa-345-717.gif",
            width: 100.0,
            height: 200.0,
            fit: BoxFit.contain,
          ),

img

CachedNetworkImage

这个可以说是 FadeInImage 的升级版,倒是并非官方widget,而是一个package。

CachedNetworkImage 中有两个非常重要的属性:

  • placeholder 展位图。这里我们可以设置loading或者别的widget等
  • errorWidget 当网络图片加载异常的时候展示

具体的使用方法和 FadeInImage 非常的相似,这里就不做演示了,大家可以自行尝试下

WillPopScope

导航的返回监听,其中回调方法中返回一个 bool 类型的值,true 为退出页面,false则反。

    @override
    Widget build(BuildContext context) {
      return WillPopScope(
        child: Scaffold(),
        onWillPop: () async {
          if (_lastTime == null ||
              DateTime.now().difference(_lastTime) >
                  Duration(milliseconds: 2000)) {
            _lastTime = DateTime.now();
            return false;
          } else {
            return showDialog(
              context: context,
              barrierDismissible: false,
              builder: (BuildContext context){
                return AlertDialog(
                  title: Text('退出App'),
                  content: Text('别怪我没告诉你哦'),
                  actions: <Widget>[
                    FlatButton(
                      child: Text('CANCEL',),
                      onPressed: (){
                        Navigator.pop(context, false);
                      },
                    ),FlatButton(
                      child: Text('SURE',),
                      onPressed: (){
                        Navigator.pop(context, true);
                      },
                    ),
                  ],
                );
              }
            );
          }
        },
      );
    }

img

Chip

标签,类似于and中的Tag,

  • avatar 左侧图标
  • labelStyle 标签样式
  • labelPadding 标签内边距
  • deleteIcon 删除图标
  • onDeleted 删除方法,必须调用才会显示删除图标
  • deleteIconColor 删除图标颜色
  • deleteButtonTooltipMessage 删除图标的提示消息
  • shape 形状
  • backgroundColor 背景颜色
  • padding 内边距
  • materialTapTargetSize 删除图标点击范围
                Chip(
                  label: Text('标签1'),
                  padding: const EdgeInsets.symmetric(horizontal: 10.0),
                  deleteIcon: Icon(Icons.clear, color: Colors.black12),
                ),
                Chip(
                  label: Text('标签2'),
                  deleteIcon: Icon(Icons.clear, color: Colors.black12),
                  labelPadding: EdgeInsets.symmetric(horizontal: 10.0),
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(3.0),
                  ),
                  onDeleted: () {
                    print(1122);
                  },
                ),
                Chip(
                  label: Text('标签3', style: new TextStyle(fontSize: 16.0)),
                  avatar: Icon(Icons.search),
                  deleteIcon: Icon(Icons.clear, color: Colors.black12),
                  labelPadding: EdgeInsets.fromLTRB(10.0, 0.0, 10.0, 0.0),
                  onDeleted: () {},
                ),

img

Spacer

简单来说,这是一个占位的widget,当然,对于布局定位我们可以使用margin来解决。但是如果抽离出组件的话,非相关属性当然还是不能放到组件内部的,或者做成可配置的。

对于占位,我们熟知的还有SizedBox,但是 SizedBox 只能指定宽度和高度,并不能适用于我们常规使用的 Flex 布局系列。而这,正是我们介绍的 Spacer 用武之地

            Row(
              children: <Widget>[
                Text('Begin'),
                Spacer(), // Defaults to a flex of one.
                Text('Middle'),
                // Gives twice the space between Middle and End than Begin and Middle.
                Spacer(flex: 2),
                Text('End'),
              ],
            ),

img

DefaultTextStyle

统一文本样式,文档的解释是,应用于没有显示指定文本样式的子代文本组件。

当然,我们可以为App整体的文本设置基调风格,通过MaterialApp中指定textTheme属性即可。但是如若是需要统一某一个页面的文本样式, DefaultTextStyle 还是首当其选。

TODO && 总结

TODO

转眼间,代码量已经不小了,demo也有点模样了,作为入门者的练习实战项目感觉也挺合适的。但是仔细雕琢其中还是有很多的瑕疵

img

  • Android、iOS的样式适配
  • 屏幕适配
  • 沸点评论功能添加
  • 沸点图片查看动画、手势检测
  • 登陆与提醒功能
  • ...

这只是刚刚开始

总结上面说道的知识点,除了一些常用的widget的使用,还有数据model的建立,路由的配置,简单动画的使用,网络请求,广播,webView等功能。基本具备了一些app所具有的功能点。但是,这一切都是指一个刚刚开始。也仅限于使用Flutter编写界面的级别,甚至于说,编写简单界面的部分。

而对于技术,我们也不能局限于使用,更在于折腾。

关于在Flutter的学习路线中,后面我个人可能回去研究下官方Example Gallery的代码,编写 Flutter 菜鸟 App 如果有精力,更愿意去学习下原生,研究下Flutter的编译过程及原理。

路漫漫其修远兮,而目前,我们至少应该熟练掌握Flutter去编写我们日常的业务需求。在逐步去探索更深的一层,去汲取、提炼。

总之,加油~

驻足思考、总结

再回首

往事如梦...

截止目前,我们基本完成了一个app该有的功能了,甚至说一个项目该有的样子了。

后面一般的页面其实也就是如此,所以写到这的同学们,已经可以完成一些基本页面的编写了。简单的说,目前已经入门了。甚至说,已经可以做App了

其实学习技术也就是这样,我们需要掌握的是“抄袭”的技巧。如何查找自己需要的widget,如何查找我们需要的 package,以及 如何查看 “widget”的源码,查看他具有的属性、以及类型

学到这里,可能有些同学会心里犯怵,我就写了这么些功能,怎么可以胜任编写App的大旗呢,比如xxx我就不会,比如xxx我也不知道。

对,不可否认,随着项目的开发,你肯定会遇到一些你不知道知识点,那么,这不就是项目经验的积累么?遇到问题,解决问题不就是自身能力的提高么?我经常说给自己的一句话这里共享给大家:三年工作经验,不是 1*1*1,而应该是 1+1+1。

Everything is decided with you

对于后面的

  • 小册 UI & 功能 编写
  • 沸点 UI & 功能 编写
  • 开源库 UI & 功能 编写
  • 活动 UI & 功能 编写
  • 登陆功能

等功能,其实很大一部分都是跟目前我们已开发的功能差不多,无非UI不同而已。当然,笔者肯定也会从中抽出来些不同的功能点,来向大家介绍。哪怕不是 web 版掘金 里面具有的。

但是我想说,后面大家的学习,尽可能多去记忆参考之前的代码,自己独立完成后续的UI编写以及自己想要的功能的编写甚至添加一些动画,尝试自己去发现问题,并解决问题。

关于学习Flutter

其实笔者也只是Flutter入门者,很多道不到的地方,还希望大家可以不啬赐教。

关于学习Flutter我只能说下个人的感受。

首先官网文档必须要看的,英文不好的同学可以去看看Flutter中文网,然后理解下Flutter的风格。刚开始的困惑困扰一定是,这么多widget,不知道哪一个才是自己应该用的,当然,这不也正是慢慢熟练的一个过程么。

其实你仔细想想,当初你学html的时候,是不是也觉得这么多的标签,也不可能全部记住呢。

然后多去看看网上别人开源的Demo,自己跟着后面学习记忆。取其精华去其糟粕。就像想在我这个demo一样,也同样会有些糟粕的地方。

然后,我想说,其实我也还在学习阶段,这本小册写完后面有时间,我会去分析官网的一个大而全的Example Flutter_Gallery ,感兴趣的同学到时候可以加群一起学习交流~

共勉~

“flutter”数据model及json处理

前言

由于我们最终是需要通过接口获取数据的,笔者个人习惯,比较喜欢先确认了字段再去进行代码的编写,所以这一章节,我们先mock下接口的数据。

从Chrome中,我copy了一份请求:list api

我们将数据copy一份到本地json中

在项目的根目录下新建一个 assets 文件夹,用于存放我们的json

  • assets/indexListData.json

内容较长,这里就不粘贴了。大家可以直接copy我的文件,也可以直接copy请求过来的数据

json

因为是资源文件的注入,所以我们需要在pubspec.yaml中注册一下

  assets:
   - assets/indexListData.json

listCell数据模型

原始数据我们有了,根据UI,我们肯定需要将list的每一个cell拆出来作为组件来使用的。

所以我们在lib目录下新建一个widgets目录用于存放我们项目中需要自定的组件

分析cell的UI样式
cell

我们来定义一个该Cell需要的数据model!

在lib目录下新建model目录

  • lib/model/indexCell.dart
  import '../util/util.dart';
  
  class IndexCell {
    bool hot;
    String isCollection;
    String tag;
    String username;
    int collectionCount;
    int commentCount;
    String title;
    String createdTime;
    String detailUrl;
  
    IndexCell(
        {this.hot,
        this.tag,
        this.username,
        this.collectionCount,
        this.createdTime,
        this.commentCount,
        this.title,
        this.detailUrl,
        this.isCollection});
  
    factory IndexCell.fromJson(Map<String, dynamic> json) {
      return IndexCell(
        hot: json['hot'],
        collectionCount: json['collectionCount'],
        commentCount: json['commentsCount'],
        tag: json['tags'][0]['title'] + '/' + json['category']['name'],
        username: json['user']['username'],
        createdTime: Util.getTimeDuration(json['createdAt']),
        title: json['title'],
        detailUrl: json['originalUrl'],
        isCollection: json['type'] ,
      );
    }
  }

如上,我们就定义了一个包含一些字段的类,因为涉及使用量很大,我们使用一个工厂构造函数,为了方便传json,这里我们再定义了一个命名构造函数 IndexCell.fromJson,而里面是对接口字段的处理赋值操作。

因为是mock(接口)过来的数据,很多时候我们都要进行一些数据格式或者字段的处理,方便我们前端UI的展示,所以这里我们在lib目录下新建一个util目录

  • lib/util/util.dart
  class Util {
  
    static String getTimeDuration(String comTime) {
      var nowTime = DateTime.now();
      var compareTime = DateTime.parse(comTime);
      if (nowTime.isAfter(compareTime)) {
        if (nowTime.year == compareTime.year) {
          if (nowTime.month == compareTime.month) {
            if (nowTime.day == compareTime.day) {
              if (nowTime.hour == compareTime.hour) {
                if (nowTime.minute == compareTime.minute) {
                  return '片刻之间';
                }
                return (nowTime.minute - compareTime.minute).toString() + '分钟前';
              }
              return (nowTime.hour - compareTime.hour).toString() + '小时前';
            }
            return (nowTime.day - compareTime.day).toString() + '天前';
          }
          return (nowTime.month - compareTime.month).toString() + '月前';
        }
        return (nowTime.year - compareTime.year).toString() + '年前';
      }
      return 'time error';
    }
  }

上面代码写的有点呆呆的,后来我查了DateTime对象,可以使用difference方法来对比两个时间差,这里就不做修改了

我们如上定义了一个处理时间的方法,复制给cell

使用mock数据和数据model

这里说下笔者个人的代码习惯,如上代码,indexPage 是我们首页UI的容器,我只想在这里一个方法拿到我要的数据,然后丢给cell去渲染,就完事了,不希望有太多关于数据的逻辑处理

所以我们在lib/util下新建一个文件 dataUtils.dart文件,用于对请求过来数据的处理和封装

  • lib/util/dataUtils.dart
    引入基础库
  import 'dart:convert';
  import '../model/indexCell.dart';
  import 'package:flutter/services.dart' show rootBundle;
  import 'dart:async' show Future;

services、async 用于我们的数据请求,虽然是读取本地json但是熟悉node应该都明白,IO当然也是异步操作。

convert用于对json数据的处理,强烈推荐文章:[译]在 Flutter 中解析复杂的
JSON

引入数据model,方便处理和吐出。

  class DataUtils {
    static Future<String> _loadIndexListAsset() async {
      return await rootBundle.loadString('assets/indexListData.json');
    }
  
    static Future<List<IndexCell>> getIndexListData() async {
      List<IndexCell> resultList = new List();
      String jsonString = await _loadIndexListAsset();
      final jsonResponseList = json.decode(jsonString)['indexListData'];
      for(int i = 0;i<jsonResponseList.length;i++){
        // resultList.add();
        IndexCell cellData = new IndexCell.fromJson(jsonResponseList[i]);
        resultList.add(cellData);
      }
      return resultList;
    }
  }

关于上面的语法已在第一章节重点说明,这里不再赘述。
方法getIndexListData吐出List<IndexCell>

在IndexPage中,我们直接如下使用

  • lib/pages/indexPage.dart
  getList(bool isLoadMore) {
    DataUtils.getIndexListData().then((resultList) {
      setState(() {
        _listData = resultList;
      });
    });
  }

方法如上,调用时机这里我们方便测试,放到initState中,并将拿到的数据丢给我们的Cell widget来测试下,是否成功了(当然,使用print就可以),注意这里 传参:isLoadMore 是为了后面做加载更多的时候使用,这里我们重点在于mock data,所以先留下这个flag,暂时方法体里并未使用该变量。

  @override
  void initState() {
    super.initState();
    getList(false);
  }
  
    @override
  Widget build(BuildContext context) {
    print(_listData.length);
    return Column(
      children: <Widget>[
        Text('IndexPage'),
        IndexListCell(cellInfo: _listData.length>0?_listData[0]:new Map())
      ],
    );
  }

上面的三目运算看起来也是呆呆的哇,没有数据就不应该渲染IndexListCell嘛,但是这里我们先这样,后面我们会加loading

cell 中对于数据Model的使用

  • widgets/indexListCell.dart
  import 'package:flutter/material.dart';
  import '../model/indexCell.dart';
  
  class IndexListCell extends StatelessWidget {
    final IndexCell cellInfo;
  
    IndexListCell({Key key,this.cellInfo}):super(key : key);
  
    @override
    Widget build(BuildContext context) {
      return Container(
        child: Text(cellInfo.username),
      );
    }
  }

如上,此刻的你,应该看到如下界面:
data

congratulation~ 成功了!

数据已经有了,下面就开始编写我们的这个indexListCellwidget吧!

总结

如上,我们完成了本地mock的数据,至此,你应该学会

  • 定义和使用数据模型
  • 异步获取本地mock data

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.