Node模块原理0926

Node模块原理

1.node模块原理分析

<body>
<!--
1.Node模块
1.1在CommonJS规范中一个文件就是一个模块
1.2在CommonJS规范中通过exports暴露数据
1.3在CommonJS规范中通过require()导入模块

2.Node模块原理分析
既然一个文件就是一个模块,
既然想要使用模块必须先通过require()导入模块
所以可以推断出require()的作用其实就是读取文件
所以要想了解Node是如何实现模块的, 必须先了解如何执行读取到的代码

3.执行从文件中读取代码
我们都知道通过fs模块可以读取文件,
但是读取到的数据要么是二进制, 要么是字符串
无论是二进制还是字符串都无法直接执行

但是我们知道如果是字符串, 在JS中是有办法让它执行的
eval  或者 new Function;

4.通过eval执行代码
缺点: 存在依赖关系, 字符串可以访问外界数据,不安全

5.通过new Function执行代码
缺点: 存在依赖关系, 依然可以访问全局数据,不安全
-->
<!--
6.通过NodeJS的vm虚拟机执行代码
runInThisContext: 无权访问外部变量, 但是可以访问global
runInNewContext:  无权访问外部变量, 也不能访问global
-->

<script>
    // let str = "console.log('www.it666.com');";
    // eval(str);

    // 存在依赖关系, 字符串可以访问外界数据,不安全
    // let name = "lnj";
    // let str = "console.log(name);";
    // eval(str);

    // let str = "console.log('www.it666.com');";
    // let fn = new Function(str);
    // console.log(fn);
    // fn();

    // 存在依赖关系, 字符串可以访问外界数据,不安全
    let name = "lnj";
    let str = "console.log(name);";
    let fn = new Function(str);
    fn();
</script>
</body>

js代码

let vm = require("vm");

// let str = "console.log('www.it666.com');";
// vm.runInThisContext(str);

/*
runInThisContext: 提供了一个安全的环境给我们自行字符串中的代码
runInThisContext提供的环境不能访问本地的变量, 但是可以访问全局的变量(也就是global上的变量)
* */
// let name = "lnj";
// let str = "console.log(name);";
// vm.runInThisContext(str); // name is not defined

// global.name = "lnj";
// let str = "console.log(name);";
// vm.runInThisContext(str);
/*
runInNewContext: 提供了一个安全的环境给我们执行字符串中的代码
runInNewContext提供的环境不能访问本地的变量, 也不能访问全局的变量(也就是global上的变量)
* */
// let name = "lnj";
// let str = "console.log(name);";
// vm.runInNewContext(str); // name is not defined

global.name = "lnj";
let str = "console.log(name);";
vm.runInNewContext(str); // name is not defined

2.node模块加载分析(多看几遍视频)

<body>
<!--
1.内部实现了一个require方法
function require(path) {
  return self.require(path);
}

2.通过Module对象的静态__load方法加载模块文件
Module.prototype.require = function(path) {
  return Module._load(path, this, /* isMain */ false);
};

3.通过Module对象的静态_resolveFilename方法, 得到绝对路径并添加后缀名
var filename = Module._resolveFilename(request, parent, isMain);

4.根据路径判断是否有缓存, 如果没有就创建一个新的Module模块对象并缓存起来
var cachedModule = Module._cache[filename];
if (cachedModule) {
   return cachedModule.exports;
}
var module = new Module(filename, parent);
Module._cache[filename] = module;

function Module(id, parent) {
  this.id = id;
  this.exports = {};
}
5.利用tryModuleLoad方法加载模块
tryModuleLoad(module, filename);
    - 6.1取出模块后缀
    var extension = path.extname(filename);

    - 6.2根据不同后缀查找不同方法并执行对应的方法, 加载模块
    Module._extensions[extension](this, filename);

    - 6.3如果是JSON就转换成对象
    module.exports = JSON.parse(internalModule.stripBOM(content));

    - 6.4如果是JS就包裹一个函数
    var wrapper = Module.wrap(content);
    NativeModule.wrap = function(script) {
        return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
    };
    NativeModule.wrapper = [
        '(function (exports, require, module, __filename, __dirname) { ',
        '\n});'
    ];
    - 6.5执行包裹函数之后的代码, 拿到执行结果(String -- Function)
    var compiledWrapper = vm.runInThisContext(wrapper);

    - 6.6利用call执行fn函数, 修改module.exports的值
    var args = [this.exports, require, module, filename, dirname];
    var result = compiledWrapper.call(this.exports, args);

    - 6.7返回module.exports
    return module.exports;
-->
</body>

3.自己实现一下(多看视频多分析)

let path = require("path");
let fs = require("fs");
let vm = require("vm");

class NJModule {
    constructor(id){
        this.id = id; // 保存当前模块的绝对路径
        this.exports = {};
    }
}
NJModule._cache = {};
NJModule._extensions = {
    ".js": function (module) {
        // 1.读取JS代码
        let script = fs.readFileSync(module.id);
        // 2.将JS代码包裹到函数中
        /*
        (function (exports, require, module, __filename, __dirname) {
            exports.name = "lnj";
        });
        * */
        let strScript = NJModule.wrapper[0] + script + NJModule.wrapper[1];
        // 3.将字符串转换成JS代码
        let jsScript = vm.runInThisContext(strScript);
        // 4.执行转换之后的JS代码
        // var args = [this.exports, require, module, filename, dirname];
        // var result = compiledWrapper.call(this.exports, args);
        jsScript.call(module.exports, module.exports);
    },
    ".json": function (module) {
        let json = fs.readFileSync(module.id);
        let obj = JSON.parse(json);
        module.exports = obj;
    }
};
NJModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
];

function njRequire(filePath) {
    // 1.将传入的相对路径转换成绝对路径
    let absPath = path.join(__dirname, filePath);
    // 2.尝试从缓存中获取当前的模块
    let cachedModule = NJModule._cache[absPath];
    if (cachedModule) {
        return cachedModule.exports;
    }
    // 3.如果没有缓存就自己创建一个NJModule对象, 并缓存起来
    let module = new NJModule(absPath);
    NJModule._cache[absPath] = module;
    // 4.利用tryModuleLoad方法加载模块
    tryModuleLoad(module);
    // 5.返回模块的exports
    return module.exports
}
function tryModuleLoad(module){
    // 1.取出模块后缀
    let extName = path.extname(module.id);
    NJModule._extensions[extName](module);
}

// let aModule = njRequire("./person.json");
let aModule = njRequire("./02-a.js");
console.log(aModule);

4.根据上面的流程,来回答几个面试题

<body>
<!--
1.NodeJS中的this为什么是一个空对象?
-->
<!--
因为所有的NodeJS文件在执行的时候都会被包裹到一个函数中, this都被修改为了空的module.exports
(function (exports, require, module, __filename, __dirname) {
    // 我们编写的代码
    // 所以说在这里面拿到的this就是 空的module.exports
});
compiledWrapper.call(module.exports, args);
-->
<!--
2.NodeJS中为什么可以直接使用exports, require, module, __filename, __dirname
-->
<!--
因为所有的NodeJS文件在执行的时候都会被包裹到一个函数中, 这些属性都被通过参数的形式传递过来了
var args = [module.exports, require, module, filename, dirname];
compiledWrapper.call(this.exports, args);
-->
<!--
3.NodeJS中为什么不能直接exports赋值, 而可以给module.exports赋值
-->
<!--
(function (exports, require, module, __filename, __dirname) {
    exports = "lnj";
});
jsScript.call(module.exports, module.exports);
return module.exports;

相当于
let exports = module.exports;
exports = "lnj";
return module.exports;
-->
<!--
4.通过require导入包时候应该使用var/let还是const?
导入包的目的是使用包而不是修改包, 所以导入包时使用const接收
-->
</body>

5.浏览器事件环

<body>
<!--
1.JS是单线程的
  JS中的代码都是串行的, 前面没有执行完毕后面不能执行

2.执行顺序
2.1程序运行会从上至下依次执行所有的同步代码
2.2在执行的过程中如果遇到异步代码会将异步代码放到事件循环中
2.3当所有同步代码都执行完毕后, JS会不断检测 事件循环中的异步代码是否满足条件
2.4一旦满足条件就执行满足条件的异步代码

3.宏任务和微任务
在JS的异步代码中又区分"宏任务(MacroTask)"和"微任务(MicroTask)"
宏任务: 宏/大的意思, 可以理解为比较费时比较慢的任务
微任务: 微/小的意思, 可以理解为相对没那么费时没那么慢的任务

4.常见的宏任务和微任务
MacroTask: setTimeout, setInterval, setImmediate(IE独有)...
MicroTask: Promise, MutationObserver ,process.nextTick(node独有) ...
注意点: 所有的宏任务和微任务都会放到自己的执行队列中, 也就是有一个宏任务队列和一个微任务队列
        所有放到队列中的任务都采用"先进先出原则", 也就是多个任务同时满足条件, 那么会先执行先放进去的

5.完整执行顺序
1.从上至下执行所有同步代码
2.在执行过程中遇到宏任务就放到宏任务队列中,遇到微任务就放到微任务队列中
3.当所有同步代码执行完毕之后, 就执行微任务队列中满足需求所有回调
4.当微任务队列所有满足需求回调执行完毕之后, 就执行宏任务队列中满足需求所有回调
... ...
注意点:
每执行完一个宏任务都会立刻检查微任务队列有没有被清空, 如果没有就立刻清空
-->
<div></div>
<button class="add">添加节点</button>
<button class="del">删除节点</button>
<script>
    //根据上面的解释分析以下代码的输出顺序
    /*
    // setImmediate和setTimeout, setInterval区别:
    // setImmediate不能设置延迟时间, 并且只能执行一次
    setImmediate(function () {
        console.log("setImmediate");
    });
    console.log("同步代码Start");
    console.log("同步代码End");
    
    */

    /*
    // MutationObserver是专门用于监听节点的变化
    let oDiv = document.querySelector("div");
    let oAddBtn = document.querySelector(".add");
    let oDelBtn = document.querySelector(".del");
    oAddBtn.onclick = function () {
        let op = document.createElement("p");
        op.innerText = "我是段落";
        oDiv.appendChild(op);
    }
    oDelBtn.onclick = function () {
        let op = document.querySelector("p");
        oDiv.removeChild(op);
    }
    let mb = new MutationObserver(function () {
        console.log("执行了");
    });
    mb.observe(oDiv, {
        "childList": true
    });
    console.log("同步代码Start");
    console.log("同步代码End");
     */

    /*
   // 1.定义一个宏任务
    setTimeout(function () {
        console.log("setTimeout1");
    }, 0);
    // 2.定义一个微任务
    Promise.resolve().then(function () {
        console.log("Promise1");
    });
    console.log("同步代码Start");
    Promise.resolve().then(function () {
        console.log("Promise2");
    });
    setTimeout(function () {
        console.log("setTimeout2");
    }, 0);
    console.log("同步代码End");
     */

    /*
    // 1.定义一个宏任务
    setTimeout(function () {
        console.log("setTimeout1");
        // 2.定义一个微任务 p1
        Promise.resolve().then(function () {
            console.log("Promise1");
        });
        // 2.定义一个微任务 p2
        Promise.resolve().then(function () {
            console.log("Promise2");
        });
    }, 0);
    // 1.定义一个宏任务
    setTimeout(function () {
        console.log("setTimeout2");
        // 2.定义一个微任务 p3
        Promise.resolve().then(function () {
            console.log("Promise3");
        });
        // 2.定义一个微任务 p4
        Promise.resolve().then(function () {
            console.log("Promise4");
        });
    }, 0);
     */

    /*
    // 1.定义一个宏任务
    setTimeout(function () {
        console.log("setTimeout1");
        // 2.定义一个微任务 p2
        Promise.resolve().then(function () {
            console.log("Promise2");
        });
        // 2.定义一个微任务 p3
        Promise.resolve().then(function () {
            console.log("Promise3");
        });
    }, 0);
    // 2.定义一个微任务 p3
    Promise.resolve().then(function () {
        console.log("Promise1");
        // s2
        setTimeout(function () {
            console.log("setTimeout2");
        });
        // s3
        setTimeout(function () {
            console.log("setTimeout3");
        });
    });
     */
</script>
</body>

6.nodeJs事件环

<body>
<!--
1.概述
和浏览器中一样NodeJS中也有事件环(Event Loop),
但是由于执行代码的宿主环境和应用场景不同,
所以两者的事件环也有所不同.

>扩展阅读: 在NodeJS中使用libuv实现了Event Loop.
>源码地址: https://github.com/libuv/libuv
>别看了C/C++语言写的, 你现在看不懂

2.NodeJS事件环和浏览器事件环区别
2.1任务队列个数不同
浏览器事件环有2个事件队列(宏任务队列和微任务队列)
NodeJS事件环有6个事件队列
2.2微任务队列不同
浏览器事件环中有专门存储微任务的队列
NodeJS事件环中没有专门存储微任务的队列
2.3微任务执行时机不同
浏览器事件环中每执行完一个宏任务都会去清空微任务队列
NodeJS事件环中只有同步代码执行完毕和其它队列之间切换的时候回去清空微任务队列
2.4微任务优先级不同
浏览器事件环中如果多个微任务同时满足执行条件, 采用先进先出
NodeJS事件环中如果多个微任务同时满足执行条件, 会按照优先级执行

2.NodeJS中的任务队列
    ┌───────────────────────┐
┌> │timers          │执行setTimeout() 和 setInterval()中到期的callback
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │pending callbacks│执行系统操作的回调, 如:tcp, udp通信的错误callback
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │idle, prepare   │只在内部使用
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │poll            │执行与I/O相关的回调
    │                  (除了close回调、定时器回调和setImmediate()之外,几乎所有回调都执行);
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │check           │执行setImmediate的callback
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└─┤close callbacks │执行close事件的callback,例如socket.on("close",func)
    └───────────────────────┘


    ┌───────────────────────┐
┌> │timers          │执行setTimeout() 和 setInterval()中到期的callback
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │poll            │执行与I/O相关的回调
    │                  (除了close回调、定时器回调和setImmediate()之外,几乎所有回调都执行);
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└─┤check           │执行setImmediate的callback
    └───────────────────────┘

1.注意点:
和浏览器不同的是没有宏任务队列和微任务队列的概念
宏任务被放到了不同的队列中, 但是没有队列是存放微任务的队列
微任务会在执行完同步代码和队列切换的时候执行

什么时候切换队列?
当队列为空(已经执行完毕或者没有满足条件回到)
或者执行的回调函数数量到达系统设定的阈值时任务队列就会切换

2.注意点:
在NodeJS中process.nextTick微任务的优先级高于Promise.resolve微任务
-->
<!--
            ┌───────────────────────┐
            │                同步代码
            └──────────┬────────────┘
                                  │
                                  │ <---- 满足条件微任务代码
                                  │
            ┌──────────┴────────────┐
        ┌> │timers          │执行setTimeout() 和 setInterval()中到期的callback
        │  └──────────┬────────────┘
        │                        │
        │                        │ <---- 满足条件微任务代码
        │                        │
        │  ┌──────────┴────────────┐
        │  │poll            │执行与I/O相关的回调
        │  │                  (除了close回调、定时器回调和setImmediate()之外,几乎所有回调都执行);
        │  └──────────┬────────────┘
        │                        │
        │                        │ <---- 满足条件微任务代码
        │                        │
        │  ┌──────────┴────────────┐
        └─┤check           │执行setImmediate的callback
            └───────────────────────┘

注意点:
执行完poll, 会查看check队列是否有内容, 有就切换到check
如果check队列没有内容, 就会查看timers是否有内容, 有就切换到timers
如果check队列和timers队列都没有内容, 为了避免资源浪费就会阻塞在poll
-->
    
    
    
<script>
//同样分析以下代码的打印顺序
/*
Promise.resolve().then(function () {
    console.log("Promise");
});
process.nextTick(function () {
    console.log("process.nextTick1");
});
process.nextTick(function () {
    console.log("process.nextTick2");
});
process.nextTick(function () {
    console.log("process.nextTick3");
});
 */
/*
setTimeout(function () {
    console.log("setTimeout");
});
Promise.resolve().then(function () {
    console.log("Promise");
});
console.log("同步代码 Start");
process.nextTick(function () {
    console.log("process.nextTick");
});
setImmediate(function () {
    console.log("setImmediate");
});
console.log("同步代码 End");
 */
/*
setTimeout(function () {
    console.log("setTimeout1");
    // p1
    Promise.resolve().then(function () {
        console.log("Promise1");
    });
    // n1
    process.nextTick(function () {
        console.log("process.nextTick1");
    });
});
console.log("同步代码 Start");
setTimeout(function () {
    console.log("setTimeout2");
    // p2
    Promise.resolve().then(function () {
        console.log("Promise2");
    });
    // n2
    process.nextTick(function () {
        console.log("process.nextTick2");
    });
});
console.log("同步代码 End");
 */

/*
注意点: 如下代码输出的结果是随机的
        在NodeJS中指定的延迟时间是有一定的误差的, 所以导致了输出结果随机的问题
*/
/*
setTimeout(function () {
    console.log("setTimeout");
}, 0);
setImmediate(function () {
    console.log("setImmediate");
});
 */

const path = require("path");
const fs = require("fs");

fs.readFile(path.join(__dirname, "04.js"), function () {
    setTimeout(function () {
        console.log("setTimeout");
    }, 0);
    setImmediate(function () {
        console.log("setImmediate");
    });
});

</script>
</body>

7.自定义本地包和全局包

<body>
<!--
1.包的规范(了解)
- package.json必须在包的顶层目录下
- 二进制文件应该在bin目录下
- JavaScript代码应该在lib目录下
- 文档应该在doc目录下
- 单元测试应该在test目录下

2.package.json字段分析(了解)
- name:包的名称,必须是唯一的,由小写英文字母、数字和下划线组成,不能包含空格
- description:包的简要说明
- version:符合语义化版本识别规范的版本字符串
    + 主版本号:当你做了不兼容的 API 修改
    + 子版本号:当你做了向下兼容的功能性新增
    + 修订号:当你做了向下兼容的问题修正
- keywords:关键字数组,通常用于搜索
- maintainers:维护者数组,每个元素要包含name、email(可选)、web(可选)字段
- contributors:贡献者数组,格式与maintainers相同。包的作者应该是贡献者数组的第一- 个元素
- bugs:提交bug的地址,可以是网站或者电子邮件地址
- licenses:许可证数组,每个元素要包含type(许可证名称)和url(链接到许可证文本的- 地址)字段
- repositories:仓库托管地址数组,每个元素要包含type(仓库类型,如git)、url(仓- 库的地址)和path(相对于仓库的路径,可选)字段
- dependencies:生产环境包的依赖,一个关联数组,由包的名称和版本号组成
- devDependencies:开发环境包的依赖,一个关联数组,由包的名称和版本号组成

3.自定义包实现步骤
1.创建一个包文件夹
2.初始化一个package.json文件
3.初始化一个包入口js文件
  注意点: 如果没有配置main, 默认会将index.js作为入口
          如果包中没有index.js, 那么就必须配置main
4.根据包信息配置package.json文件
  注意点: 通过scripts可以帮我们记住指令, 然后通过npm run xxx方式就可以执行该指令
          如果指令的名称叫做start或者test, 那么执行的时候可以不加run
5.给package.json添加bin属性, 告诉系统执行全局命令时需要执行哪一个JS文件
6.在全局命令执行的JS文件中添加 #! /usr/bin/env node
7.通过npm link 将本地包放到全局方便我们调试

4.将自定义包发布到官网
1.在https://www.npmjs.com/注册账号
2.在终端输入npm addUser
3.在终端输入npm publish
-->
</body>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容

  • 面试题一:https://github.com/jimuyouyou/node-interview-questio...
    R_X阅读 1,623评论 0 5
  • 1-------- 走进前端 2-------- jQuery 3-------- CSS 4-------- A...
    依依玖玥阅读 2,323评论 0 34
  • 总结一: [node.js总结](http://www.cnblogs.com/Darren_code/archi...
    xiumeiii阅读 1,890评论 0 14
  • 诸事不顺呐呐呐 知道是自己做的不够 就是不能再认真点再努力20%吗 你看别人英语演讲第一 考什么过什么 面什么过什...
    夏一喵阅读 188评论 13 0
  • 人生的路很长,在这条长长的路上,每个人会遇到很多选择。这些选择有大有小,但都或多或少地改变了我们的人生。 ...
    polo的简书阅读 164评论 0 0