时间:2022-06-20 10:26:03 | 栏目:JavaScript代码 | 点击:次
这里我们一起从 0 开始搭建一个组件系统。首先通过上一篇《前端组件化基础知识》中知道,一个组件可以通过 Markup 和 JavaScript 访问的一个环境。
所以我们的第一步就是建立一个可以使用 markup 的环境。这里我们会学习使用两种建立 markup 的风格。
第一种是基于与 React 一样的 JSX 去建立我们组件的风格。第二种则是我们去建立基于类似 Vue 的这种,基于标记语言的 Parser 的一种风格。
JSX 在大家一般认知里面,它是属于 React 的一部分。其实 Facebook 公司会把 JSX 定义为一种纯粹的语言扩展。而这个 JSX 也是可以被其他组件体系去使用的。
甚至我们可以把它单独作为一种,快捷创建 HTML 标签的方式去使用。
那么我们就从最基础的开始,首先我们需要创建一个新的项目目录:
mkdir jsx-component
在你们喜欢的目录下创建这个项目文件夹。建立好文件夹之后,我们就可以进入到这个目录里面并且初始化 npm
。
npm init
执行以上命令之后,会出现一些项目配置的选项问题,如果有需要可以自行填写。不过我们也可以直接一直按回车,然后有需要的同学可以后面自己打开 package.json
自行修改。
Wepack 很多同学应该都了解过,它可以帮助我们把一个普通的 JavaScript 文件变成一个能把不同的 import 和 require 的文件给打包到一起。
所以我们需要安装 webpack
,当然我们也可以直接使用 npx 直接使用 webpack,也可以全局安装 webpack-cli。
那么这里我们就使用全局安装 webpack-cli:
npm install -g webpack webpack-cli
安装完毕之后,我们可以通过输入下面的一条命令来检测一下安装好的 webpack 版本。如果执行后没有报错,并且出来了一个版本号,证明我们已经安装成功了。
webpack --version
因为 JSX 它是一个 babel 的插件,所以我们需要依次安装 webpack,babel-loader, babel 和 babel 的 plugin。
这里使用 Babel 还有一个用处,它可以把一个新版本的 JavaScript 编译成一个老版本的 JavaScript,这样我们的代码就可以在更多老版本的浏览器中运行。
安装 Babel 我们只需要执行以下的命令即可。
npm install --save-dev webpack babel-loader
这里我们需要注意的是,我们需要加上 --save-dev
,这样我们就会把 babel 加入到我们的开发依赖中。
执行完毕后,我们应该会看到上面图中的消息。
为了验证我们是正确安装好了,我们可以打开我们项目目录下的 package.json
。
{ "name": "jsx-component", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "devDependencies": { "babel-loader": "^8.1.0", "webpack": "^5.4.0" } }
好,我们可以看到在 devDependencies
下方,确实是有我们刚刚安装的两个包。还是担心的同学,可以再和 package.json
确认一下眼神哈。
到这里我们就需要配置一下 webpack。配置 webpack 我们需要创建一个 webpack.config.js
配置文件。
在我们项目的根目录创建一个 webpack.config.js
文件。
首先 webpack config 它是一个 nodejs 的模块,所以我们需要用 module.exports 来写它的设置。而这个是早期 nodejs 工具常见的一种配置方法,它用一个 JavaScript 文件去做它的配置,这样它在这个配置里面就可以加入一些逻辑。
module.exports = {}
Webpack 最基本的一个东西,就是需要设置一个 entry (设置它的入口文件)。这里我们就设置一个 main.js
即可。
module.exports = { entry: "./main.js" }
这个时候,我们就可以先在我们的根目录下创建一个 main.js
的文件了。在里面我们先加入一个简单的 for
循环。
// main.js 文件内容 for (let i of [1, 2, 3]) { console.log(i); }
这样 webpack 的基本配置就配置好了,我们在根目录下执行一下 webpack 来打包一下 main.js
的文件来看看。需要执行下面的这行命令进行打包:
webpack
执行完毕之后,我们就可以在命令行界面中看到上面这样的一段提示。
注意细节的同学,肯定要举手问到,同学同学!你的命令行中报错啦!黄色部分确实有给我们一个警告,但是不要紧,这个我们接下的配置会修复它的。
这个时候我们会发现,在我们的根目录中生成了一个新的文件夹 dist
。这个就是 webpack 打包默认生成的文件夹,我们所有打包好的 JavaScript 和资源都会被默认放入这个文件夹当中。
这里我们就会发现,这个 dist
文件夹里面有一个打包好的 main.js
的文件,这个就是我们写的 main.js
,通过 webpack 被打包好的版本。
然后我们打开它,就会看到它被 babel 编译过后的 JavaScript 代码。我们会发现我们短短的几行代码被加入了很多的东西,这些其实我们都不用管,那都是 Webpack 的 “喵喵力量”。
在代码的最后面,还是能看到我们编写的 for
循环的,只是被改造了一下,但是它的作用是一致的。
接下来我们来安装 babel-loader,其实 babel-loader 并没有直接依赖 babel 的,所以我们才需要另外安装 @babel/core
和 @babel/preset-env
。我们只需要执行下面的命令行来安装:
npm install --save-dev @babel/core @babel/preset-env
最终的结果就如上图一样,证明安装成功了。这个时候我们就需要在 webpack.config.js
中配置上,让我们打包的时候用上 babel-loader。
在我们上面配置好的 webpack.config.js
的 entry
后面添加一个选项叫做 module
。
然后模块中我们还可以加入一个 rules
,这个就是我们构建的时候所使用的规则。而 rules
是一个数组类型的配置,这里面的每一个规则是由一个 test
和一个 use
组成的。
test:
test
的值是一个正则表达式,用于匹配我们需要使用这个规则的文件。这里我们需要把所有的 JavaScript 文件给匹配上,所以我们使用 /\.js/
即可。use: loader:
babel-loader
的名字即可options:
presets:
@babel/preset-env
最后我们的配置文件就会是这个样子:
module.exports = { entry: './main.js', module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], }, }, }, ], }, };
这样配置好之后,我们就可以来跑一下 babel 来试一试会是怎么样的。与刚才一样,我们只需要在命令行执行 webpack
即可。
如果我们的配置文件没有写错,我们就应该会看到上面图中的结果。
然后我们进入 dist
文件夹,打开我们编译后的 main.js
,看一下我们这次使用了 babel-loader 之后的编译结果。
编译后的结果,我们会发现 for of
的循环被编译成了一个普通的 for
循环。这个也可以证明我们的 babel-loader 起效了,正确把我们新版本的 JavaScript 语法转成能兼容旧版浏览器的 JavaScript 语法。
到了这里我们已经把 JSX 所需的环境给安装和搭建完毕了。
最后我们还需要在 webpack.config.js 里面添加一个环境配置,不过这个是可加也可不加的,但是我们为了平时开发中的方便。
所以我们需要在 webpack.config.js 中添加一个 mode
,这我们使用 development
。这个配置表示我们是开发者模式。
一般来说我们在代码仓库里面写的 webpack 配置都会默认加上这个 mode: 'development'
的配置。当我们真正发布的时候,我们就会把它改成 mode: 'production'
。
module.exports = { entry: './main.js', mode: 'development', module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], }, }, }, ], }, };
改好之后,我们在使用 webpack
编译一下,看看我们的 main.js
有什么区别。
显然我们发现,编译后的代码没有被压缩成一行了。这样我们就可以调试 webpack 生成的代码了。这里我们可以注意到,我们在 main.js
中的代码被转成字符串,并且被放入一个 eval()
的函数里面。那么我们就可以在调试的时候把它作为一个单独的文件去使用了,并且可以进行断点调试。
万事俱备,只欠东风了,最后我们需要如何引入 JSX呢?在引入之前,我们来看看,如果就使用现在的配置在我们的 main.js
里面使用 JSX 语法会怎么样。作为程序员的我们,总得有点冒险精神!
所以我们在 main.js
里面加入这段代码:
var a = <div/>
然后大胆地执行 webpack 看看!
好家伙!果然报错了。这里的报错告诉我们,在 =
后面不能使用 “小于号”,但是在正常的 JSX 语法中,这个其实是 HTML 标签的 “尖括号”,因为没有 JSX 语法的编译过程,所以 JavaScript 默认就会认为这个就是 “小于号”。
所以我们要怎么做让我们的 webpack 编译过程支持 JSX 语法呢?这里其实就是还需要我们加入一个最关键的一个包,而这个包名非常的长,叫做 @babel/plugin-transform-react-jsx
。执行以下命令来安装它:
npm install --save-dev @babel/plugin-transform-react-jsx
安装好之后,我们还需要在 webpack 配置中给他加入进去。我们需要在 module
里面的 rules
里面的 use
里面加入一个 plugins
的配置,然后在其中加入 ['@babel/plugin-transform-react-jsx']
。
然后最终我们的 webpack 配置文件就是这样的:
module.exports = { entry: './main.js', mode: 'development', module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], plugins: ['@babel/plugin-transform-react-jsx'], }, }, }, ], }, };
配置好之后,我们再去执行一下 webpack。这时候我们发现没有再报错了。这样也就证明我们的代码现在是支持使用 JSX 语法了。
最后我们来围观一下,最后编程的效果是怎么样的。
我们会发现,在 eval
里面我们加入的 <div/>
被翻译成一个 React.createElement("div", null)
的函数调用了。
所以接下来我们就一起来看一下,我们应该怎么实现这个 React.createElement
,以及我们能否把这个换成我们自己的函数名字。
首先我们来尝试理解 JSX,JSX 其实它相当于一个纯粹在代码语法上的一种快捷方式。在上一部分的结尾我们看到,JSX语法在被编译后会出现一个 React.createElement
的调用。
那么这里我们就先修改在 webpack 中的 JSX 插件,给它一个自定义的创建元素函数名。我们打开 webpack.config.js,在 plugins 的位置,我们把它修改一下。
module.exports = { entry: './main.js', mode: 'development', module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], plugins: [ [ '@babel/plugin-transform-react-jsx', { pragma: 'createElement' } ] ], }, }, }, ], }, };
上面我们只是把原来的 ['@babel/plugin-transform-react-jsx']
参数改为了 [['@babel/plugin-transform-react-jsx', {pragma: 'createElement'}]]
。加入了这个 pragma
参数,我们就可以自定义我们创建元素的函数名。
这么一改,我们的 JSX 就与 React 的框架没有任何联系了。我们执行一下 webpack 看一下最终生成的效果,就会发现里面的 React.createElement
就会变成 createElement
。
接下来我们加入一个 HTML 文件来执行我们的 main.js 试试。首先在根目录创建一个 main.html
,然后输入一下代码:
<script src="./main.js"></script>
然后我们执行在浏览器打开这个 HTML 文件。
这个时候我们控制台会给我们抛出一个错误,我们的 createElement
未定义。确实我们在 main.js
里面还没有定义这个函数,所以说它找不到。
所以我们就需要自己编写一个 createElement
这个函数。我们直接打开根目录下的 main.js
并且把之前的 for
循环给删除了,然后加上这段代码:
function createElement() { return; } let a = <div />;
这里我们就直接返回空,先让这个函数可以被调用即可。我们用 webpack 重新编译一次,然后刷新我们的 main.html 页面。这个时候我们就会发现报错没有了,可以正常运行。
在我们的编译后的代码中,我们可以看到 JSX 的元素在调用 createElement 的时候是传了两个参数的。第一个参数是 div
, 第二个是一个 null
。
这里第二个参数为什么是 null
呢?其实第二个参数是用来传属性列表的。如果我们在 main.js 里面的 div 中加入一个 id="a"
,我们来看看最后编译出来会有什么变化。
我们就会发现第二个参数变成了一个以 Key-Value 的方式存储的JavaScript 对象。到这里如果我们想一下,其实 JSX 也没有那么神秘,它只是把我们平时写的 HTML 通过编译改写成了 JavaScript 对象,我们可以认为它是属于一种 “[[语法糖]]”。
但是 JSX 影响了代码的结构,所以我们一般也不会完全把它叫作语法糖。
接下来我们来写一些更复杂一些的 JSX,我们给原本的 div 加一些 children 元素。
function createElement() { return; } let a = ( <div id="a"> <span></span> <span></span> <span></span> </div> );
最后我们执行以下 webpack 打包看看效果。
在控制台中,我们可以看到最后编译出来的结果,是递归的调用了 createElement
这个函数。这里其实已经形成了一个树形的结构。
父级就是第一层的 div 的元素,然后子级就是在后面当参数传入了第一个 createElement 函数之中。然后因为我们的 span 都是没有属性的,所以所有后面的 createElement 的第二个参数都是 null
。
根据我们这里看到的一个编译结果,我们就可以分析出我们的 createElement 函数应有的参数都是什么了。
type
―― 就是这个标签的类型attribute
―― 标签内的所有属性与值...children
―― 这里我们使用了 JavaScript 之中比较新的语法 ...children
表示把后面所有的参数 (不定个数) 都会变成一个数组赋予给 children 变量那么我们 createElement
这个函数就可以写成这样了:
function createElement(type, attributes, ...children) { return; }
函数我们有了,但是这个函数可以做什么呢?其实这个函数可以用来做任何事情,因为这个看起来长的像 DOM API,所以我们完全可以把它做成一个跟 React 没有关系的实体 DOM。
比如说我们就可以在这个函数中返回这个 type
类型的 element
元素。这里我们把所有传进来的 attributes
给这个元素加上,并且我们可以给这个元素挂上它的子元素。
创建元素我们可以用 createElement(type)
,而加入属性我们可以使用 setAttribute()
,最后挂上子元素就可以使用 appendChild()
。
function createElement(type, attributes, ...children) { // 创建元素 let element = document.createElement(type); // 挂上属性 for (let attribute in attributes) { element.setAttribute(attribute); } // 挂上所有子元素 for (let child of children) { element.appendChild(child); } // 最后我们的 element 就是一个节点 // 所以我们可以直接返回 return element; }
这里我们就实现了 createElement
函数的逻辑。最后我们还需要在页面上挂?d上我们的 DOM 节点。所以我们可以直接挂?d在 body 上面。
// 在 main.js 最后加上这段代码 let a = ( <div id="a"> <span></span> <span></span> <span></span> </div> ); document.body.appendChild(a);
这里还需要注意的是,我们的 main.html 中没有加入 body 标签,没有 body 元素的话我们是无法挂?d到 body 之上的。所以这里我们就需要在 main.html 当中加入 body 元素。
<body></body> <script src="dist/main.js"></script>
好,这个时候我们就可以 webpack 打包,看一下效果。
Wonderful! 我们成功的把节点生成并且挂?d到 body 之上了。但是如果我们的 div
里面加入一段文字,这个时候就会有一个文本节点被传入我们的 createElement
函数当中。毋庸置疑,我们的 createElement
函数以目前的逻辑是肯定无法处理文本节点的。
接下来我们就把处理文本节点的逻辑加上,但是在这之前我们先把 div 里面的 span 标签删除,换成一段文本 “hello world”。
let a = <div id="a">hello world</div>;
在我们还没有加入文本节点的逻辑之前,我们先来 webpack 打包一下,看看具体会报什么错误。
首先我们可以看到,在 createElement
函数调用的地方,我们的文本被当成字符串传入,然后这个参数是接收子节点的,并且在我们的逻辑之中我们使用了 appendChild
,这个函数是接收 DOM 节点的。显然我们的文本字符串不是一个节点,自然就会报错。
通过这种调试方式我们可以马上定位到,我们需要在哪里添加逻辑去实现这个功能。这种方式也可以算是一种捷径吧。
所以接下来我们就回到 main.js
,在我们挂上子节点之前,判断以下 child 的类型,如果它的类型是 “String” 字符串的话,就使用 createTextNode()
来创建一个文本节点,然后再挂?d到父元素上。这样我们就完成了字符节点的处理了。
function createElement(type, attributes, ...children) { // 创建元素 let element = document.createElement(type); // 挂上属性 for (let name in attributes) { element.setAttribute(name, attributes[name]); } // 挂上所有子元素 for (let child of children) { if (typeof child === 'string') child = document.createTextNode(child); element.appendChild(child); } // 最后我们的 element 就是一个节点 // 所以我们可以直接返回 return element; } let a = <div id="a">hello world</div>; document.body.appendChild(a);
我们用这个最新的代码 webpack 打包之后,就可以在浏览器上看到我们的文字被显示出来了。
到了这里我们编写的 createElement
已经是一个比较有用的东西了,我们已经可以用它来做一定的 DOM 操作。甚至它可以完全代替我们自己去写 document.createElement
的这种反复繁琐的操作了。
这里我们可以验证以下,我们在 div 当中重新加上我们之前的三个 span, 并且在每个 span 中加入文本。11
let a = ( <div id="a"> hello world: <span>a</span> <span>b</span> <span>c</span> </div> );
然后我们重新 webpack 打包后,就可以看到确实是可以完整这种 DOM 的操作的。
现在的代码已经可以完成一定的组件化的基础能力。
之前我们都是在用一些,HTML 自带的标签。如果我们现在把 div 中的 d 改为大写 D 会怎么样呢?
let a = ( <Div id="a"> hello world: <span>a</span> <span>b</span> <span>c</span> </Div> );
果不其然,就是会报错的。不过我们找到了问题根源的关键,这里我们发现当我们把 div 改为 Div 的时候,传入我们 createElement
的 div 从字符串 ‘div' 变成了一个 Div
类。
当然我们的 JavaScript 中并没有定义 Div 类,这里自然就会报 Div 未定义的错误。知道问题的所在,我们就可以去解决它,首先我们需要先解决未定义的问题,所以我们先建立一个 Div 的类。
// 在 createElment 函数之后加入 class Div {}
然后我们就需要在 createElement
里面做类型判断,如果我们遇到的 type 是字符类型,就按原来的方式处理。如果我们遇到是其他情况,我们就实例化传过来的 type
。
function createElement(type, attributes, ...children) { // 创建元素 let element; if (typeof type === 'string') { element = document.createElement(type); } else { element = new type(); } // 挂上属性 for (let name in attributes) { element.setAttribute(name, attributes[name]); } // 挂上所有子元素 for (let child of children) { if (typeof child === 'string') child = document.createTextNode(child); element.appendChild(child); } // 最后我们的 element 就是一个节点 // 所以我们可以直接返回 return element; }
这里我们还有一个问题,我们有什么办法可以让自定义标签像我们普通 HTML 标签一样操作呢?在最新版的 DOM 标准里面是有办法的,我们只需要去注册一下我们自定义标签的名称和类型。
但是我们现行比较安全的浏览版本里面,还是不太建议这样去做的。所以在使用我们的自定义 element 的时候,还是建议我们自己去写一个接口。
首先我们是需要建立标签类,这个类能让任何标签像我们之前普通 HTML 标签的元素一样最后挂?d到我们的 DOM 树上。
它会包含以下方法:
mountTo()
―― 创建一个元素节点,用于后面挂?d到 parent
父级节点上setAttribute()
―― 给元素挂上所有它的属性appendChild()
―― 给元素挂上所有它的子元素首先我们来简单实现以下我们 Div
类中的 mountTo
方法,这里我们还需要给他加入 setAttribute
和 appendChild
方法,因为在我们的 createElement
中有挂?d属性子元素的逻辑,如果没有这两个方法就会报错。但是这个时候我们先不去实现这两个方法的逻辑,方法内容留空即可。
class Div { setAttribute() {} appendChild() {} mountTo(parent) { this.root = document.createElement('div'); parent.appendChild(this.root); } }
这里面其实很简单首先给类中的 root
属性创建成一个 div 元素节点,然后把这个节点挂?d到这个元素的父级。这个 parent
是以参数传入进来的。
然后我们就可以把我们原来的 body.appendChild 的代码改为使用 mountTo
方法来挂?d我们的自定义元素类。
// document.body.appendChild(a); a.mountTo(document.body);
用现在的代码,我们 webpack 打包看一下效果:
我们可以看到我们的 Div 自定义元素是有正确的被挂?d到 body 之上。但是 Div 中的 span 标签都是没有被挂?d上去的。如果我们想它与普通的 div 一样去工作的话,我们就需要去实现我们的 setAttribute
和 appendChild
逻辑。
接下来我们就一起来尝试完成剩余的实现逻辑。在开始写 setAttribute 和 appendChild 之前,我们需要先给我们的 Div 类加入一个构造函数 constructor
。在这里个里面我们就可以把元素创建好,并且代理到 root
上。
constructor() { this.root = document.createElement('div'); }
然后的 setAttribute
方法其实也很简单,就是直接使用 this.root
然后调用 DOM API 中的 setAttribute
就可以了。而 appendChild
也是同理。最后我们的代码就是如下:
class Div { // 构造函数 // 创建 DOM 节点 constructor() { this.root = document.createElement('div'); } // 挂?d元素的属性 setAttribute(name, attribute) { this.root.setAttribute(name, attribute); } // 挂?d元素子元素 appendChild(child) { this.root.appendChild(child); } // 挂?d当前元素 mountTo(parent) { parent.appendChild(this.root); } }
我们 webpack 打包一下看看效果:
我们可以看到,div 和 span 都被成功挂?d到 body 上。也证明我们自制的 div 也能正常工作了。
这里还有一个问题,因为我们最后调用的是 a.mountTo()
,如果我们的变量 a
不是一个自定义的元素,而是我们普通的 HTML 元素,这个时候他们身上是不会有 mountTo
这个方法的。
所以这里我们还需要给普通的元素加上一个 Wrapper 类,让他们可以保持我们元素类的标准格式。也是所谓的标准接口。
我们先写一个 ElementWrapper
类,这个类的内容其实与我们的 Div 是基本一致的。唯有两个区别
type
到我们的构造函数,并且用这个 type 去建立我们的 DOM 节点this.root.appendChild
,因为所有普通的标签都被改为我们的自定义类,所以 appendChild 的逻辑需要改为 child.mountTo(this.root)
class ElementWrapper { // 构造函数 // 创建 DOM 节点 constructor(type) { this.root = document.createElement(type); } // 挂?d元素的属性 setAttribute(name, attribute) { this.root.setAttribute(name, attribute); } // 挂?d元素子元素 appendChild(child) { child.mountTo(this.root); } // 挂?d当前元素 mountTo(parent) { parent.appendChild(this.root); } } class Div { // 构造函数 // 创建 DOM 节点 constructor() { this.root = document.createElement('div'); } // 挂?d元素的属性 setAttribute(name, attribute) { this.root.setAttribute(name, attribute); } // 挂?d元素子元素 appendChild(child) { child.mountTo(this.root); } // 挂?d当前元素 mountTo(parent) { parent.appendChild(this.root); } }
这里我们还有一个问题,就是遇到文本节点的时候,是没有转换成我们的自定义类的。所以我们还需要写一个给文本节点,叫做 TextWrapper
。
class TextWrapper { // 构造函数 // 创建 DOM 节点 constructor(content) { this.root = document.createTextNode(content); } // 挂?d元素的属性 setAttribute(name, attribute) { this.root.setAttribute(name, attribute); } // 挂?d元素子元素 appendChild(child) { child.mountTo(this.root); } // 挂?d当前元素 mountTo(parent) { parent.appendChild(this.root); } }
有了这些元素类接口后,我们就可以改写我们 createElement
里面的逻辑。把我们原本的 document.createElement
和 document.createTextNode
都替换成实例化 new ElementWrapper(type)
和 new TextWrapper(content)
即可。
function createElement(type, attributes, ...children) { // 创建元素 let element; if (typeof type === 'string') { element = new ElementWrapper(type); } else { element = new type(); } // 挂上属性 for (let name in attributes) { element.setAttribute(name, attributes[name]); } // 挂上所有子元素 for (let child of children) { if (typeof child === 'string') child = new TextWrapper(child); element.appendChild(child); } // 最后我们的 element 就是一个节点 // 所以我们可以直接返回 return element; }
然后我们 webpack 打包一下看看。
没有任何意外,我们整个元素就正常的被挂?d在 body 的上了。同理如果我们把我们的 Div 改回 div 也是一样可以正常运行的。
当然我们一般来说也不会写一个毫无意义的这种 Div 的元素。这里我们就会写一个我们组件的名字,比如说 Carousel
,一个轮播图的组件。
完整代码 ―― 对你有用的话,就给我一个 ⭐️ 吧,谢谢!
我们在这里互相监督,互相鼓励,互相努力走上人生学习之路,让学习改变我们生活!
学习的路上,很枯燥,很寂寞,但是希望这样可以给我们彼此带来多一点陪伴,多一点鼓励。我们一起加油吧! (๑ •̀ㅂ•́)و