2018年6月

遇到一个跨域问题,说到这里大家就心里默然一笑了,因为不外乎就那样的balabala,但这次有点不一样的在于:后端确定自己已经开启了跨域!这...就有点尴尬了

于是问题变成,我是怎么判断跨域的?

首先是最简单的http://10.63.231.79:8080http://47.106.111.100:8088发post请求,无疑已经跨域了,

其次,因为我的是vue-cli搭建的(我负责webview部分),然后用的axios发请求,按api地址来发数据的时候,报错403,如下图:

看起来,我很有道理,但是有两个问题

第一,为什么postman可以成功呢?

第二,为什么安卓端的请求也可以成功呢?

安卓端的请求如下:

第一个问题

通过为什么使用postman发送请求时不会有跨域问题?得知,

准确的跨域定义:当一个资源从与该资源本身所在的服务器不同的域或端口请求一个资源时,资源会发起一个跨域 HTTP 请求,通俗一点,正常的跨域情况,是你访问了一个A网站,然后这个网站返回的资源里面,请求了B网站/端口的资源,于是就跨域了

跨域这个情况只会出现在浏览器页面里,因为实际上是浏览器由于安全原因限制了这些请求的访问。在postman里面,实际上每发出一个请求,都是在独立请求一个资源

还有很解惑的一句话:

应该是POSTMAN中有个类似java程序或者node 代理了跨域接口,先用node去请求跨域接口,得到数据,然后本地发起ajax请求,实际请求的是node代理过后的接口,就不存在跨域了

只有在浏览器端发 ajax 请求才会存在跨域

http请求不会存在跨域

先不说正确性,其中的那句,实际请求的是node代理过后的接口,立马联想到,这应该也是设置代理能解决跨域的原因吧(将ajax请求交给代理变成由代理发出http请求),也就是句子最后的,只有在浏览器端发ajax请求才会存在跨域,http请求不会存在跨域

第二个问题

的确不明白,但似乎也是这样的理由吧?

后询问安卓的同学是使用的okhttp,上面赫然写着这样一句话:

OkHttp is an HTTP client

这不就差不多又是一个postman吗...似乎到这里事情已经很明白了,但我们需要再看一下的是

浏览器的报错信息:

Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://10.63.231.79:8080' is therefore not allowed access. The response had HTTP status code 403.

什么是preflight request?

参考链接: HTTP访问控制(CORS)-MDN

规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是GET 以外的 HTTP请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求

"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

简单请求

另联想到,遇到的axios跨域(也就是上述的报错),搜索到的axios可以解决跨域访问的问题吗?,说道:

server端不支持跨域,如果不是自己开发的,那么可以自己写个后端转发该请求,用代理的方式实现。(后来的自己就是这样解决跨域的)

server端支持跨域,但不能响应OPTIONS请求时,如果 server 端也支持简单请求(见下方定义),特别是 axios POST请求时,默认使用 JSON 格式,改成 string 问题就解决了!Using application/x-www-form-urlencoded format,需要转成application/x-www-form-urlencoded,可以用qs.stringify(data),那么这里又是为什么可以呢?

重点就在于简单请求,查看上述MDN的介绍:

某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”

若请求满足所有下述条件,则该请求可视为“简单请求”:

其中条件有(还有其他,请看文档):

Content-Type 的值仅限于下列三者之一:text/plainmultipart/form-dataapplication/x-www-form-urlencoded

原来这个原理就是,把跨域的请求,通过转换成简单请求,这样就不会触发了OPTIONS了,但我的确这样做了之后,发送的预检请求报200了,但是结果控制台还是报跨域的错...这?

注意这些跨域请求与浏览器发出的其他跨域请求并无二致。如果服务器未返回正确的响应首部,则请求方不会收到任何数据。因此,那些不允许跨域请求的网站无需为这一新的 HTTP 访问控制特性担心。

额...原来白高兴一场...

通过响应头部来判断是否开跨域

后续再看MDN文档时更找到一个有力的证据判断服务器是否开跨域:

假如站点 http://foo.example 的网页应用想要访问 http://bar.other 的资源,请求报文和响应报文:

GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
Origin: http://foo.example


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61 
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

请求首部字段 Origin 表明该请求来源于 http://foo.exmaple,响应中携带了响应首部字段 Access-Control-Allow-Origin。使用OriginAccess-Control-Allow-Origin 就能完成最简单的访问控制

Access-Control-Allow-Origin: * 表明,该资源可以被任意外域访问

Access-Control-Allow-Origin: http://foo.example 表明,仅允许来自 http://foo.example 的访问

这就指明了一个更强的方法判断服务器开没开跨域:在响应中,查看是否有Access-Control-Allow-Origin字段

想到怎么测试呢? 用githubapi啊:

可以看到githubapi完全支持跨域

事至此,可以看到这个证明题基本已经差不多了,那就是:服务器没开跨域!

接着就是,你是怎么解决的跨域?

首先一开始由于不确定是不是跨域的问题,因此换了三种发ajax请求的框架,axiosvue-resource重识vue-resource ),jquery,但结果其实都会报错,

其中用jquerypost请求到后端的时候,用jsonp来发post请求,来解决跨域:

$.ajax('http://47.106.111.100:8088/rest/app/register', {
    type: "POST",
    data: test,
    dataType: 'jsonp',
    crossDomain: true,
    success: function (data) {
        alert('hello');
        if (data && data.resultcode == '200') {
            console.log(data.result.today);
        }
    }
});

还是会出现Cross-Origin Read Blocking (CORB) blocked cross-origin response

那么这是为什么呢?

jsonp为什么不支持post请求?

首先定义jsonp: 创建一个 script 标签,将 src 设置为目标请求,插入到 dom 中,服务器接受该请求并返回数据,数据通常被包裹在 回调钩子

可以用 jsonp 发送 post 请求么?

显然不行,看过支持 post 请求的 script 么?

详细解释:

从前端和后端两个角度分析jsonp跨域访问(完整实例)

Why does my JavaScript get a “No 'Access-Control-Allow-Origin' header is present on the requested resource” error when Postman does not?

Response to preflight request doesn't pass access control check

总结:

也就是jsonp:

  1. 不支持post请求
  2. 需要后台配合(You need the server to return data in JSONP format too)

跨域的解决有:

If you do NOT want to:

  • Disable web security in Chrome
  • Use JSONP
  • Use a third party site to re-route your requests

或者

You are running into CORS issues.There are several ways to fix this.

于是凉凉...,post请求肯定触发跨域,而jsonp是不行了,联想到之前有过印象,webpack可设置代理来解决跨域问题(大概是上面postman提到的那种思想),于是查一下,get!

webpack 前后端分离开发接口调试解决方案,proxyTable解决方案

http-proxy-middleware

config/index.js里面找到proxyTable

proxyTable: {
    "/rest": "http://47.106.111.100:8088"
},

这样的话,请求到 /rest/app/register,现在会被代理到请求 http://47.106.111.100:8088/rest/app/register。 更多的用法查看上面的http-proxy-middleware文档即可

就是这样,all is done !

受同学所托,要爬一个网站

第一步很自然的,查看网页元素,看渲染的能不能直接抓下来,

但是用上requestcherrio,一查元素,为空?(页面上明明有),把requestbody打印出来,才发现是ajax,应对方法:打开chrome,用Network,终于查看到了请求,复制链接地址,竟然直接浏览器打开就可以获得到json数据...

接下来就是漫长的数据整理工作了以及一直要弄明白的回调地狱的问题

第一关:字符串处理

以为返回的是json,结果一直报错,才发现返回的内容是hxbase_json1({sum: 2660, list: [,…]}),也就是真正的json内容在两个括号里面...

换言之,怎么获得两个指定的符号中间的内容?

第一反应:正则,但基本忘光了(以前就只是生搬硬套的用...),之后搜索中看到有人提到了直接用字符串的方法,str.substring(str.indexOf('('),str.indexOf(')')),这方法好是好,但是:

input: "hxbase_json1({XXXXxx(xxx)xxxx})"
output: ({XXXXxx(xxx

也就是如果中间有括号就不行(弃),如果要用字符串的方法的话,直接提取子串不就可以了!str.slice(13, -1);,主要发现其他请求都是返回的这个形式,所以直接写死也行,好的,第一关成功!

另,就在写总结的时候,发现找到了以前第一次爬网站的时候各种堆砌出来的较通用的正则匹配函数:

// 匹配前后缀,提取中间的内容
function getInnerString(source, prefix, postfix) {
    if (prefix !== "<p>")
        var regexp = new RegExp(encodeReg(prefix) + '.+' + encodeReg(postfix), 'gi');
    else
        var regexp = new RegExp(encodeReg(prefix) + '(?:(?!<\/p>)[\\s\\S])*' + encodeReg(postfix), 'gi');
    var matches = String(source).match(regexp);
    if (matches == null) {
        let now = new Date();
        matches = ['this is failed', now + ''];
    }
    var formatedMatches = matches.map(value => {
        return value
            .replace(prefix, '')
            .replace(postfix, '');
    });
    return formatedMatches;
}
//转义影响正则的字符
function encodeReg(source) {
    return String(source).replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1');
}

第二关:字符串转换成json

以为提取出字符串之间的内容就可以了

// 提取中间内容
let str = body.slice(13, -1);
// 把字符串转化为json
let testJson = JSON.parse(str);
...
// 报错
SyntaxError: Unexpected token s in JSON at position 1

参考链接:字符串转换成JSON的三种方式,终于知道为什么,因为必须要遵守json规范,而字符串里面的属性并没有引号...

示例发现一个更神奇的现象,只有外面用单引号,而属性用双引号才可以

let jee = '{"sum":2660}'; // 正确:输出{ sum: 2660 }
let jee = "{'sum':2660}"; // SyntaxError: Unexpected token ' in JSON at position 1
console.log(JSON.parse(jee));

于是这里的解决方法只能用eval了(第一次觉得eval这么亲切!),完美!

第三关:回调地狱

现在整个流程差不多写成这样:

// 部分为伪代码
for(let i= 11; i<=17 ;i++) {
    // 先请求起始url,借此得到所有的页码数
    let first = "xxx";
    Request(first, (error, response, body)=> {
        let str = body.slice(13, -1);
        let testJson = eval("(" + str + ")");
        let sum = testJson.sum;
        // 获取到整个的页码
        let pageNum = Math.floor(sum / 20);
        for(let j=1; j<=pageNum; j++) {
            let url = "xxx"+j;
            Request(url, (error, response, body) => {
                // 获取数据(伪代码)
                list.push(data)
            })
        }
        //写数据
        fs.writeFile(i+"year.json", JSON.stringify(list),
            function (err) {
                if (err) throw err;
                console.log('It\'s saved!');
            }
        );
    })
}

这也就是传说中的回调金字塔(回调地狱),说说我自己遇到的情况,

第一,不一定每次请求都能正确无误,所以常常跑着跑着,突然报错了,然后这样的代码结构简直要命,实际中的我还在其中用了一个urls数组先存下所有的url再遍历发请求,也就是各种嵌套,各种异步,出错出在哪到找不到...

第二,遇到了爬虫(或者异步中)很经典的问题,怎么判断回调结束了?这里的问题就是在fs.writeFile的时候怎么确保list已经获取到了所有的值?(11年有133条url,每条url指向20条数据)

第一种问题,以前有过一次爬博客园的经历,那里是采用Promise化的方式解决的,request库还有promise的版本,promise就保证了要么then回调成功,要么err

/**
 * 第一次爬博客园时的经历
 * users 示例
 {
    raphael5200: {
        username: 'raphael5200',
        defaultPage: 'http://www.cnblogs.com/raphael5200/default.html?page=',
        pages: 'http://www.cnblogs.com/raphael5200/default.html?page=2'
    },
    joyeecheung: {
        username: 'joyeecheung',
        defaultPage: 'http://www.cnblogs.com/joyeecheung/default.html?page=',
        pages: 'http://www.cnblogs.com/joyeecheung/default.html?page=2'
    }
    ...
 }
*/
// 遍历每一个用户
for (let key in users) {
    // 请求每一个用户的主页
    rq(users[key].pages)
      .then((body)=>{
        let titles = [];
        let reads = 0;
        let replys = 0;
        for (let i = 0; i < getPageNum(body); i++) {
          // 请求用户发出来的每一个文章列表页
          rq(users[key].defaultPage + '' + i)
            .then((body) => {
              // 提取当前文章列表页的所有文章标题,存到users[key].articles, 每篇文章的阅读数、评论数相加放置到readNum,replyNum中

              // 写入 当时不知道怎么判断异步结束,直接用了一个比较hack的方法,不断的重写文件(最后能保存下来的就随缘了)
              // fs.writeFile: Asynchronously writes data to a file, replacing the file if it already exists
              fs.writeFile("users.json", JSON.stringify(users),
                  function (err) {
                      if (err) throw err;
                      console.log('It\'s saved!');
                  }
              );
            }).catch (function (err) {
              //console.log(err);
            });
        }
      })
      .catch(function (err) {
          console.log(err);
      });
};
// 写入 还尝试过另一个方法,等待1分钟之后写入...(随缘)
setTimeout((users) => {
fs.writeFile("users.json",JSON.stringify(users),
  function (err) {
    if (err) throw err;
    console.log('It\'s saved!');
  }
);
}, 1000*1*60);

这次实际上是通过,把不必要的循环去掉,直接把10年17年要的数据,就分为8条url,在直接请求这8条(好在只有8条啊)

插播一条小细节: 之前的let str = body.slice(13, -1);,常常报错,大致意思就是body出问题了,undefined或者什么的不能调用slice方法,这样的话就又遇到了一个经典的问题:js怎么判断变量的类型?

参考链接:JavaScript学习总结(六)——JavaScript判断数据类型总结

// 外层加一个判断就避免了
if (typeof body === "string") {
        let str = body.slice(13, -1);

第二种问题是采用的,settInterval计数解决

参考链接:NodeJS的异步编程风格

例如:fs去访问五个txt文件,1.txt...5.TXT

const fs = require('fs');
const list = [];

for (let i = 1; i <= 5; i++ ) {
    fs.readFile('./' + i + '.txt', 'utf-8' ,(err, data) => {
        if (err) throw err;
        list.push(data);
    });
}

// 在后台不断的轮训,当list的长度达到指定值(也就是回调全部结束),此时就可以进行清除定时器
let intervalId = setInterval(() => {
    console.log('waiting!...');
    if(list.length === 5) {
        console.log('SUCCESS');
        // 注意setInterval与clearInterval的配合
        clearInterval(intervalId);
    }
}, 1000);

插话:有点疑惑的是,clearInterval(intervalId)有种自身清除自身的感觉? 已知的,它们都是隶属于WindowOrWorkerGlobalScope的方法 (待填坑..)

但是在参考链接中的方法更佳,使用setInterval会加入新的事件循环? Javascript 单线程模型 Event Loop 机制

//记录总的文件大小,使用一个`count`计数来判断异步结束 
count = filenames.length;
for (i = 0; i < filenames.length; i++) {
  fs.stat("./" + filenames[i], function (err, stats) {
     totalBytes += stats.size;
     count--;
     if (count === 0) {
           console.log(totalBytes);
      }
   });
}
从这个例子中,我们可以学到一点:并发运行的相同异步函数如果协作完成任务,需要添加计数代码判断执行状态,并且把所有异步函数完成后执行的代码放在判断条件的语句块里

第三关: 中文乱码

前面的关卡好不容易闯过了后,兴高采烈的抓到了数据,写到了json里,可是一打开一开,gg,股票名称的字段,全是乱码?,网上提示,直接看一下源网站的编码,浏览器打开一看头部,charset="gb2312",难怪...

参考链接:

request库使用时, gb2312、GBK中文乱码解决方法

nodejs下request模块中文gb2312乱码问题

也就是引用iconv-lite包,对返回的gb2312body进行一下转码,Iconv.decode(body, 'gb2312').toString(),这样就可以了

数据库备份:

参考官方文档:

Upgrading MySQL on Windows,了解自己按哪种方式升级,我的是Upgrading MySQL Using the Windows ZIP Distribution

之后开始数据备份,Database Backup Methods

选择备份的方法, 我用的是Making Backups with mysqldump,去查看mysqldump的用法,mysqldump — A Database Backup Program文档比较多,我直接看的:Using mysqldump for Backups

// 直接使用命令行,报错 `Got error: 1045: Access denied for user 'ODBC'@'localhost' (using password: NO)`
mysqldump --all-databases > dump.sql

在网上找到Mysql备份还原数据库之mysqldump实例及参数详细说明

// 成功
mysqldump -u root -p --all-databases > dump.sql

新版本的安装

选用傻瓜版:msi,安装过程有一个注意事项,因为选的是Custom,但是在后来的界面中一直打开的mysql server都是最新版的8.0,而且安装到某个界面还没有next弹出...,后来才发现在Custom之后的选择界面有filter选项,点开,里面就可以选择各种版本的server了

数据的导入

进入mysql数据库,使用source命令:

source E:\xxx.sql

起因,妹子问的实习的公司连不上网,只有公司电脑连有线才可以,但是她在输ip地址从192.168.101.100192.168.101.254一个个试(已经试到206),还是连不上网怎么办?

其实我对这个连不上网的问题真的好菜啊...,仅有的一次还是利用192.168.1.1(还是什么去了..)登录上路由器,然后路由器密码还是贼简单,登上去了,就改了一个路由器的名字,开心了半天,然后就没有然后了...

回到正题,于是赶快google,知乎搜,还是不知道怎么做,就知道了一个DHCP静态ip的名词.. 以及看到网友推荐了一本书,网络是怎样连接的,mark一下了

接着只好很丢脸的说,我不会啊,但是妹子说的从192.168.101.100192.168.101.254一个个试,却留下了印象,就是,我们会用ping来测试网络是不是畅通,那么有没有一种方法能让自动的ping呢?

也就是今天自己学会的bat脚本:

刚开始搜到这个:shell脚本实现网络连接的检测,这里面的思路就用循环从0到254,依次ping,很好,不过这是shell脚本,也就是在我的电脑上装了git-bash是可以运行的,但是妹子的电脑是没有的,也就是要转化成bat脚本啊,于是坑就来了

bat脚本的格式语法问题:

@echo off ::开头,不显示后续命令行及当前命令行

set var_name = "hello" ::set来定义变量

echo %var_name%   ::echo用于输出,变量取值用%var%

for /l %%i in (1,1,254)  ::递增循环的示例 /l表示带开关,此处的变量是%%i
do (
    :: code here     
)

于是代码初步这样:

@echo off
::Ping网段所有IP
set ip="192.168.0."  
for /l %%i in (1,1,254)
do (
ping -c 2 %ip%%%i |grep -q 'ttl=' && echo "%ip%%%i yes"|| echo "%ip%%%i no"
)
::yes正常,no主机不存在或不正常   
pause

遇到的麻烦:

一闪而过...,应该是语法错误了,直接在cmd中输入ping,根本没有-c的参数,而且grepSearch for PATTERN in each FILE or standard input,都去掉,依然一闪而过...

后来改成for /l %%i in (1,1,254) do ( ping %ip%%%i && echo "%ip%%%i yes"|| echo "%ip%%%i no" ),写成一行可以,但报

错误的参数 1。
""192.168.0." 1 no"

在循环中拼接字符串,ping %ip%%%i 难道不可行?

批处理文件在循环中拼接字符串,为什么结果不对啊问题中发现也有人遇到过,是需要设置环境变量延迟,加上setlocal enabledelayedexpansion,并变成ping !ip!%%i,可还是报同样的错误

难道是%%i是数字类型,而ip是字符串类型根本不行?

在bat批处理脚本中,怎样将for语句中的%%i当作字符串处理?做类似于%str:~1,5%之类的操作?

可以用在循环中在设一个变量等于%%i的方法,即set num= %%i ping !ip!!num!,结果没了错误参数,变成

""192.168.0." 1 yes"

这到底好没好呢...

同时又有提到字符串拼接的批处理用的是set /a num+=1set str=!str! %%i ,再在cmd批处理中set /a和set /p的区别介绍,了解到

/P 命令行开关允许将变量数值设成用户输入的一行输入

/a 是指定一个变量等于一串运算字符

彩蛋是这里面的实例2竟然有ping的示例代码

@echo off
set a=1
:start
echo %a%
ping 172.19.5.%a% -w 1 -n 1|find /i "Lost = 1"&&set c=1||set c=0
if %c%==0 (echo 172.19.5.%a% >>IP.txt)
set /a a=%a%+1
if %a%==255 exit
goto :start

利用goto变成一个do...while的结构,而且巧妙的用172.19.5.%a%避免了之前的字符串拼接,最后还能把ping成功的ip写入txt

自己粘过来试,仍不成功...因为明明在我的电脑上172.19.5.x都是ping不通的,却也全部写入了txt文件

再次查看ping命令:发现-w为等待每次回复的超时时间(毫秒),这里设置为1,-n为要发送的回显请求数,而find,作者是想找到Lost=1的ping不通的,但是我的是中文...,设为丢失=1就好了:

1
数据包:已发送 = 1,已接受 = 0, 丢失 = 1 (100%丢失)

完美!

还有一种方式,之前看到设置超时为1,觉得是超时太断所致,即使是ping通的也会被判断为错误的,就直接改成,去掉find,并需要把判断条件改成%c%==1

ping 192.168.101.%a% -n 1 && set c=1 || set c=0
if %c%==1 (echo 192.168.101.%a% >>IP.txt)

这样也是可行的,但是每次都会输出一大堆的ping的回显消息了

其实过程中还有,各种报ECHO处于关闭状态的错误等,从下午到晚上,也算是第一次知道了shell编程是什么,以及借此好像阴差阳错的懂了一点点linux的强大

最后希望自己慢慢的不要那么菜吧哈哈

还有部分的参考连接:

.bat批处理(三):变量声明、设置、拼接、截取

BAT批处理为何报错echo处于关闭状态?

易百教程-批处理简介

网文记录_Winter 直播笔记

来自:计算机之子 Winter 直播笔记及我的学习方法论

  1. 对于框架的使用没必要花太多时间,应该多研究一下三大框架背后的设计思想
  2. 当一个程序员对算法、语言标准、底层、原生、英文文档这些词汇产生恐惧感的时候他的技术生命已经走到尽头。
  3. 前端架构主要解决的是高复用性,架构能力提升方向主要是组件库开发、前端框架实现等。
  4. 对于前端进阶这个问题,其实看书的作用和意义已经不太明显,需要寻找好的平台和合适的项目,在项目中不断克服难题并挑战自己,遇到问题再去查资料总结。如果只是闭门看书那很难成为高手,书只是基础而已,真正的应用还是在项目中。
  5. 寒冬中能做的只有提升自己,但是光靠技术是不行的。
  6. 推荐 TensorFlow、可视化切图、PWA、WebGL

Jeff Bezos' speech to Princeton

Tomorrow, in a very real sense, your life -- the life you another from scrath on your own --begins

How will you use your gifts? What choices will you make? What inertia be your guide, or Will you follow your passions?

Will you follow dogma, or Will you be original? Will you choose a life of ease, or a life of service and adventure?

Will you wilt under criticism, or Will you follow yoour convictions?

Will you bluff it out when you're wrong, or Will you apologize?

Will you guard your heart against rejection, or Will you act when you fall in love?

Will you play it safe, or Will you be a little bit swashbuckling?

When it's tough, Will you give up, or Will you be relentless?

Will you be a cynic, or Will you be a builder?

Will you be clever at the expense of others, or Will you be kind?

I will hazard a prediction, when you are 80 years old, and in a quiet moment of reflection narrating for only yourself, the most personal version of your life story.

the telling that will be most compact and meaningful will be the series of choices you have made,

in the end, We are our choices, Build yourself a great story.

TK-answer:

艺无止境,功不唐捐