- 快召唤伙伴们来围观吧
- 微博 QQ QQ空间 贴吧
- 文档嵌入链接
- 复制
- 微信扫一扫分享
- 已成功复制到剪贴板
百度San框架教程
展开查看详情
1 .
2 . 目 录 致谢 README 指南 数据 什么东西不要保存在 data 里? 什么东西可以保存在data里? data bind时的auto camel 试图模板 如何遍历一个对象? 数组深层更新如何触发视图更新? 如何实现元素的显示/隐藏? 组件间通信 父组件如何更新子组件? 子组件如何通知父组件? 子组件与更高层组件如何通信? 动态子组件如何传递消息给父组件? 组件管理 我们可以操作 DOM 吗? 路由管理 如何使用 san-router 建立一个单页应用的后台系统? 应用状态管理 如何使用 san-store 实现后台系统的状态管理? FAQ Q&A集锦 如何处理绝对定位组件的 DOM? 教程 安装 背景 开始 模板 数据操作 数据校验 样式 条件 循环 事件处理 本文档使用 书栈(BookStack.CN) 构建 - 2 -
3 . 表单 插槽 过渡 组件 组件反解(old) 组件反解 服务端渲染 API 主模块API 组件API 本文档使用 书栈(BookStack.CN) 构建 - 3 -
4 .致谢 致谢 当前文档 《百度San框架教程》 由 进击的皇虫 使用 书栈(BookStack.CN) 进行构建,生成 于 2018-07-05。 书栈(BookStack.CN) 仅提供文档编写、整理、归类等功能,以及对文档内容的生成和导出工 具。 文档内容由网友们编写和整理,书栈(BookStack.CN) 难以确认文档内容知识点是否错漏。如果 您在阅读文档获取知识的时候,发现文档内容有不恰当的地方,请向我们反馈,让我们共同携手,将知 识准确、高效且有效地传递给每一个人。 同时,如果您在日常工作、生活和学习中遇到有价值有营养的知识文档,欢迎分享到 书栈 (BookStack.CN) ,为知识的传承献上您的一份力量! 如果当前文档生成时间太久,请到 书栈(BookStack.CN) 获取最新的文档,以跟上知识更新换 代的步伐。 文档地址:http://www.bookstack.cn/books/san-zh 书栈官网:http://www.bookstack.cn 书栈开源:https://github.com/TruthHun 分享,让知识传承更久远! 感谢知识的创造者,感谢知识的分享者,也感谢每一位阅读到此处的 读者,因为我们都将成为知识的传承者。 本文档使用 书栈(BookStack.CN) 构建 - 4 -
5 .README README San Website prepare 1. $ npm i preview 1. # npm start 2. hexo s deploy 1. # npm run deploy 2. $ hexo deploy 给 San 文档做贡献 San 是一个传统的 MVVM 组件框架,官网地址 https://baidu.github.io/san/ 如果你在使用 San 过程中遇到任何问题,请通过 github: https://github.com/baidu/san 给我们提 issue; 如果你在看 San 文档的过程中发现任何问题,可以通过文档 github: https://github.com/baidu/san-website 给我们提 issue,或者在相应位置修改后发起 PR; 当然我们非常欢迎您在实践 San 框架过程中,有任何实践经验或总结文档,可以通过 PR 的方式提交 到文档 github: https://github.com/baidu/san-website, 经过我们 review 后的文档会 合入 San 的官方文档中。 San 文档 PR 规范 1. 提交前的 fork 同步更新操作:每次 PR 前请进行 fork 同步更新操作,避免产生冲突。 本文档使用 书栈(BookStack.CN) 构建 - 5 -
6 .README 2. 文档内容: 必须包含对实践问题的原理分析总结,包含实际的 demo,demo 的编写请使用 codepen,最后将其嵌入到文档中,具体详情及嵌入方式请见例子: https://baidu.github.io/san/practice/traverse-object/. 3. 实践类文档项目路径: [ ] 添加文档可以往这里发 PR 1. https://github.com/baidu/san- website/tree/master/source/_posts/practice [ ] 链接是手工加 1. https://github.com/baidu/san- website/blob/master/themes/san/layout/practice.ejs 4. PR 标题与内容:PR 标题和内容,请对文档进行详细说明,并提供文档的最终截图。 有任何文档问题可以给我们提 issue 来源(书栈小编注) 本文档使用 书栈(BookStack.CN) 构建 - 6 -
7 .指南 指南 数据 试图模板 组件间通信 组件管理 路由管理 应用状态管理 FAQ 如何处理绝对定位组件的 DOM? 本文档使用 书栈(BookStack.CN) 构建 - 7 -
8 .数据 数据 什么东西不要保存在 data 里? 什么东西可以保存在data里? data bind时的auto camel 本文档使用 书栈(BookStack.CN) 构建 - 8 -
9 .什么东西不要保存在 data 里? 什么东西不要保存在 data 里? title: 什么东西不要保存在 data 里? categories: - practice 我们知道,data 里应该存与视图相关的数据状态。我们在下面列举了一些不当的使用场景,这些场景 是我们不止发现过一次的。 函数 不要把函数作为数据存放。函数应该是独立的,或者作为组件方法存在。 1. // bad 2. this.data.set('camel2kebab', function (source) { 3. return source.replace(/[A-Z]/g, function (match) { 4. return '-' + match.toLowerCase(); 5. }); 6. }); DOM 对象 这个应该不用解释吧。 1. // bad 2. this.data.set('sideEl', document.querySelector('sidebar')); 组件等复杂对象 不要使用数据来做动态子组件的管理。动态子组件对象可以直接存在组件的成员中。 1. // bad 2. var layer = new Layer(); 3. layer.attach(document.body); 4. this.data.set('layer', layer); 5. 6. // good 7. var layer = new Layer(); 8. layer.attach(document.body); 本文档使用 书栈(BookStack.CN) 构建 - 9 -
10 .什么东西不要保存在 data 里? 9. this.layer = layer; 本文档使用 书栈(BookStack.CN) 构建 - 10 -
11 .什么东西可以保存在data里? 什么东西可以保存在data里? 在 San 的文档中写道:” San 是一个 MVVM 的组件框架,通过 San 的视图引擎能够让用户只用操 作数据,视图自动更新”。这里要说的 data 指的就是用户操作的”数据”。data 里应该存与视图相关 的数据状态。 data 中保存的数据 对于一个组件,data 数据的来源可分为如下两种: 1、组件自身定义的数据; 2、从父组件中传入的数据; 3、computed 中定义的数据; 以上三种,我们都可以通过 this.data.get() 方法获取。 组件自身定义的数据(状态)保存在该组件的 data 中,可以对其进行修改从而影响当前组件以及子 组件的视图。从父组件传入的数据,可以从 data 中获取,但通常我们只是使用这个数据,如果要更 改从父组件传入的数据,虽然可以直接在组件内更改,但通常的做法是到该数据初始化的地方去更改。 data 数据应该是纯数据 data 数据可以是字符串、数值、 数组、原生对象这样的纯数据。对于正则表达式,纯函数这样的, 如果是在组件自身使用,则需要外部引入,或者作为组件的方法。 但如果要传递给子组件使用,则可 以存放在 data 中。 例如,在表单组件中,我们可能会在业务层自定义验证方法,传入子组件使用。 本文档使用 书栈(BookStack.CN) 构建 - 11 -
12 .data bind时的auto camel data bind时的auto camel 在 san 组件中,data 的键值必须遵守 camelCase (驼峰式)的命名规范,不得使用 kebab-case (短横线隔开式)规范。 场景一 当一个父组件调用子组件并进行 data 绑定时,如果某一项属性写法使用了 kebab-case,san 会 自动将其转换为 camelCase,然后传入子组件。下面的一个例子说明了这一点: 示例一 1. class Child extends san.Component { 2. static template = ` 3. <ol> 4. <li>{{dataParent}}</li> 5. <li>{{data-parent}}</li> 6. </ol> 7. `; 8. } 9. 10. class Parent extends san.Component { 11. static template = ` 12. <div> 13. <san-child data-parent="data from parent!"/> 14. </div> 15. `; 16. 17. static components = { 18. 'san-child': Child 19. }; 20. } 21. 22. new Parent().attach(document.body); See the Pen vJQgWm by Ma Lingyang (@mly-zju) on CodePen. 分析 上面例子中,父组件调用子组件,为 data-parent 属性传入了”data from parent!”字符串。在 本文档使用 书栈(BookStack.CN) 构建 - 12 -
13 .data bind时的auto camel 子组件中,同时在li标签中输出 dataParent 和 data-parent 属性的值,可以看 到, dataParent 打印出的正是父组件绑定的值,作为对比, data-parent 并没有输出我们期望 的绑定值。从这个例子中可以很明显看出,对于传入的属性键值,san会自动将 kebab-case 写法转 换为 camelCase。而作为对比,在原生 html 标签中,并不会有 auto-camel 的特性,我们如果 传入一个自定义的 kebab-case 写法的属性,依然可以通过 dom.getAttribute('kebab- case') 来进行读取。san 的 template 与原生 html 的这一点不同值得我们注意。 在这个场景中的 auto camel 是很有迷惑性的,这个特性很容易让我们误以为在开发中,定义组件的 属性键值时候我们可以随心所欲的混用 camelCase 和 kebab-case,因为反正 san 会自动帮我们 转换为 camelCase 形式。那么,实际上是不是如此呢?来看场景二。 场景二 在场景一中,父组件为子组件绑定了一个 kebab-case 写法的属性,被自动转换为 camelCase。那 么在子组件中,如果自身返回的初始 data 属性本身就是 kebab-case 类型,又会出现怎样的情况 呢?我们看第二个例子: 示例二 1. class Child extends san.Component { 2. static template = ` 3. <ol> 4. <li>{{dataSelf}}</li> 5. <li>{{data-self}}</li> 6. </ol> 7. `; 8. 9. initData() { 10. return { 11. 'data-self': 'data from myself!' 12. } 13. } 14. } 15. 16. new Child().attach(document.body); See the Pen QMJpvL by Ma Lingyang (@mly-zju) on CodePen. 分析 在上面例子中,Child 组件初始 data 中包含一项键值为 data-self 的数据。我们将其分别 本文档使用 书栈(BookStack.CN) 构建 - 13 -
14 .data bind时的auto camel 以 dataSelf 和 data-self 打印到 li 标签中,可以看到,两种都没有正确打印出我们初始化的 值。说明对于自身 data 属性而言,如果属性的键值不是 camelCase 的形式,san 并不会对其进 行 auto camel 转换,所以我们无论以哪种方式,都无法拿到这个数据。 原理分析 在 san 的 compile 过程中,对 template 的解析会返回一个 ANODE 类的实例。其中 template 中绑定属性的时候,属性对象的信息会解析为 ANODE 实例中的 props 属性。对于子组 件来说,会根据父组件的 aNode.props 来生成自身的 data binds。 在 san 中,非根组件做 data binds 过程中,接受父组件的 aNode.props 这一步时,会做 auto camel 处理。这就解释了上述两个例子为什么父组件 kebab 属性传入后,子组件 camel 属 性表现正常,其余情况都是异常的。事实上在 san 的源码中,我们可以找到相关的处理函数: 1. function kebab2camel(source) { 2. return source.replace(/-([a-z])/g, function (match, alpha) { 3. return alpha.toUpperCase(); 4. }); 5. } 6. 7. function camelComponentBinds(binds) { 8. var result = new IndexedList(); 9. binds.each(function (bind) { 10. result.push({ 11. name: kebab2camel(bind.name), 12. expr: bind.expr, 13. x: bind.x, 14. raw: bind.raw 15. }); 16. }); 17. 18. return result; 19. } 在生成子组件的绑定过程中,正是由于调用了 camelComponentBinds 这个函数,所以才有 auto camel 的特性。 结论 san 的 auto camel 只适用于父组件调用子组件时候的数据绑定。对于一个组件自身的初始数据, 如果属性为 kebab-case,我们将无法正确拿到数据。因此,在写 san 组件的过程中,无论何时, 本文档使用 书栈(BookStack.CN) 构建 - 14 -
15 .data bind时的auto camel 对于 data 中的属性键值,我们都应该自觉地严格遵循 camelCase 规范。 本文档使用 书栈(BookStack.CN) 构建 - 15 -
16 .试图模板 试图模板 如何遍历一个对象? 数组深层更新如何触发视图更新? 如何实现元素的显示/隐藏? 本文档使用 书栈(BookStack.CN) 构建 - 16 -
17 .如何遍历一个对象? 如何遍历一个对象? 在San中已经提供了 san-for 指令(可以简写为 s-for )将 Array 渲染为页面中的列表,那么 对于 Object 想要进行遍历并渲染应当怎么做呢?由于 San 的指令并不直接支持 Object 的遍 历,因此可以使用计算属性进行对象的遍历 使用 1. class MyComponent extends San.component { 2. static computed = { 3. list() { 4. let myObject = this.data.get('myObject'); 5. return Object.keys(myObject).map(item => { 6. return { 7. key: item, 8. value: myObject[item] 9. } 10. }); 11. } 12. }; 13. } 示例 See the Pen san-traverse-object by liuchaofan (@asd123freedom) on CodePen. 本文档使用 书栈(BookStack.CN) 构建 - 17 -
18 .数组深层更新如何触发视图更新? 数组深层更新如何触发视图更新? 在 San 组件中,对数据的变更需要通过 set 或 splice 等方法,实现用最简单的方式,解决 兼容性的问题,同时为了保证数据操作的过程可控,San 的数据变更在内部是 Immutable 的,因此 遇到数组深层做数据交换时直接 set 数据会发现没有触发视图的更新 场景描述 1. class MyApp extends san.Component { 2. static template = ` 3. <div> 4. <div 5. style="cursor: pointer" 6. on-click="handlerClick($event)">点我交换数据</div> 7. <ul> 8. <li s-for="item in list">{{item.title}}</li> 9. </ul> 10. </div> 11. `; 12. initData() { 13. return { 14. list: [ 15. { 16. title: 'test1' 17. }, 18. { 19. title: 'test2' 20. } 21. 22. ] 23. }; 24. } 25. handlerClick() { 26. 27. // 想交换两个值 28. let firstNews = this.data.get('list'); 29. let firstData = firstNews[0]; 30. let secondData = firstNews[1]; 31. firstNews[1] = firstData; 32. firstNews[0] = secondData; 33. 本文档使用 书栈(BookStack.CN) 构建 - 18 -
19 .数组深层更新如何触发视图更新? 34. // 在这里直接set数据发现并没有触发视图的更新 35. this.data.set('list', firstNews); 36. } 37. } 38. 39. let myApp = new MyApp(); 40. myApp.attach(document.body); 原因分析 San 的 data 的数据是 Immutable 的,因此 set firstNews 时变量的引用没变, diff 的时 候还是相等的,不会触发更新。 解决方式如下 See the Pen 数组深层更新触发视图更新 by solvan(@sw811) on CodePen. 本文档使用 书栈(BookStack.CN) 构建 - 19 -
20 .如何实现元素的显示/隐藏? 如何实现元素的显示/隐藏? 通过 s-if 指令,我们可以为元素指定条件。只有当条件成立时元素才会渲染,否则元素不会被加 载。 但 s-if 无法实现这样的需求:我们需要在符合条件的情况下显示某元素,条件不满足时,元素在页 面中隐藏,但依然被挂载到 DOM 。这个时候,元素的展现用 CSS 控制更为合适。 这一需求的本质可以归纳为:如何根据条件实现元素的显示/隐藏。 如何处理 San 提供在视图模板中进行样式处理的方案,详见教程。你可以用不同的 class 控制样式,也可以 用 inline 样式实现。 1. 用 class 控制元素的显示与隐藏 1. <!-- template --> 2. <div> 3. <ul class="list{{isHidden ? ' list-hidden' : ' list-visible'}}"></ul> 4. </div> 注意, class 属性有多个类名时,需要为第一个以后的类名加上空格。 codepen 演示如下: See the Pen 根据条件添加不同样式-用class控制 by MinZhou (@Mona_) on CodePen. CSS 控制着样式的展现,所以 DOM 始终都存在页面节点树中。你可以打开控制台看看。 2. 用内联样式控制元素的隐藏与显示 1. <!-- template --> 2. <div> 3. <ul style="display: {{isHidden ? 'none' : 'block'}}">visible</ul> 4. </div> See the Pen 根据条件添加不同样式-用内联样式控制 本文档使用 书栈(BookStack.CN) 构建 - 20 -
21 .如何实现元素的显示/隐藏? by MinZhou (@Mona_) on CodePen. 有时候数据可能并不存在,所以把样式名包含在插值中更为可靠。 1. <!-- template --> 2. <div> 3. <ul style="{{isHidden === false ? 'display: none' : 'display: block'}}">visible</ul> 4. </div> 3. 使用计算属性 前面的两种方案都可以通过使用计算属性,将判断逻辑从模板中解耦出来,以便更好的应对可能变得更 为复杂的需求。下面是基于 class 的例子: 1. san.defineComponent({ 2. template: ` 3. <div> 4. <ul class="{{ulClass}}"></ul> 5. </div> 6. `, 7. computed: { 8. ulClass() { 9. const isHidden = this.data.get('isHidden'); 10. if (isHidden) { 11. return 'list list-hidden'; 12. } 13. return 'list list-visible'; 14. } 15. } 16. }) codepen 演示如下: See the Pen 基于 computed 的元素显示隐藏 by LeuisKen (@LeuisKen) on CodePen. 4. 使用 filter filter 也可以用于对 class 和 style 进行处理,解耦的效果和 computed 类似,其特点是能 够显式地声明属性值与数据的依赖关系。下面是基于 class 的例子: 本文档使用 书栈(BookStack.CN) 构建 - 21 -
22 .如何实现元素的显示/隐藏? 1. san.defineComponent({ 2. template: ` 3. <div> 4. <ul class="{{isHidden | handleHidden}}"></ul> 5. </div> 6. `, 7. filters: { 8. handleHidden(isHidden) { 9. if (isHidden) { 10. return 'list list-hidden'; 11. } 12. return 'list list-visible'; 13. } 14. } 15. }) codepen 演示如下: See the Pen 基于 filter 的元素显示隐藏 by LeuisKen (@LeuisKen) on CodePen. 我们可以很明显地看出,class 是由 isHidden 控制的。 这里要额外注意的是,如果和 class 关联的有多个 data ,用 filter 的方法可能会有一些问 题,比如我在下面的例子中实现了一个tab组件: 1. san.defineComponent({ 2. template: ` 3. <div class="tab"> 4. <div 5. s-for="tab in tabs" 6. class="{{tab.value | mapActive}}" 7. on-click="tabChange(tab.value)" 8. > 9. {{tab.name}} 10. </div> 11. </div> 12. `, 13. initData() { 14. return { 15. active: '', 16. tabs: [ 17. { 本文档使用 书栈(BookStack.CN) 构建 - 22 -
23 .如何实现元素的显示/隐藏? 18. name: '第一项', 19. value: 'one' 20. }, 21. { 22. name: '第二项', 23. value: 'two' 24. } 25. ] 26. }; 27. }, 28. tabChange(value) { 29. this.data.set('active', value); 30. }, 31. filters: { 32. mapActive(value) { 33. const active = this.data.get('active'); 34. const classStr = 'sm-tab-item'; 35. if (value === active) { 36. return classStr + ' active'; 37. } 38. return classStr; 39. } 40. } 41. }); codepen 演示如下: See the Pen 没有显式声明依赖的tab bug演示 by LeuisKen (@LeuisKen) on CodePen. 此处当我在点击 tab 的时候,虽然 active 能够正常更新,但是视图不会引起变化,因为 San 的 依赖收集机制不认为 active 的修改会影响到视图,因此需要我们在模板中显式声明对 active 的 依赖,参考如下代码: 1. san.defineComponent({ 2. // 将下面的 mapActive 改成 mapActive(active),显示声明视图对 active 的依赖 3. template: ` 4. <div class="tab"> 5. <div 6. s-for="tab in tabs" 7. class="{{tab.value | mapActive(active)}}" 8. on-click="tabChange(tab.value)" 9. > 10. {{tab.name}} 本文档使用 书栈(BookStack.CN) 构建 - 23 -
24 .如何实现元素的显示/隐藏? 11. </div> 12. </div> 13. `, 14. initData() { 15. return { 16. active: '', 17. tabs: [ 18. { 19. name: '第一项', 20. value: 'one' 21. }, 22. { 23. name: '第二项', 24. value: 'two' 25. } 26. ] 27. }; 28. }, 29. tabChange(value) { 30. this.data.set('active', value); 31. this.fire('change', value); 32. }, 33. filters: { 34. // 这里就不需要通过 this.data.get('active') 拿到 active 了 35. mapActive(value, active) { 36. const classStr = 'sm-tab-item'; 37. if (value === active) { 38. return classStr + ' active'; 39. } 40. return classStr; 41. } 42. } 43. }); codepen 演示如下: See the Pen 显式声明依赖的tab演示 by LeuisKen (@LeuisKen) on CodePen. 通过在模板中显示声明视图对 active 的依赖, San 就能正常更新视图了。这也是为什么我会在一 开始说 filter 的特点是能够显式地声明属性值与数据的依赖关系。 结语 本文档使用 书栈(BookStack.CN) 构建 - 24 -
25 .如何实现元素的显示/隐藏? 隐藏和显示是开发中较为常见的需求,还有一些其他的样式切换需求,使用以上两种方法都可以轻松实 现。 总结一下,如果你要控制元素的渲染与否(是否添加到节点树),你需要使用 s-if 指令;如果你仅 仅只想控制 DOM 节点的样式,比如元素的显示/隐藏样式,请使用数据控制 class 或内联样式。 本文档使用 书栈(BookStack.CN) 构建 - 25 -
26 .组件间通信 组件间通信 父组件如何更新子组件? 子组件如何通知父组件? 子组件与更高层组件如何通信? 动态子组件如何传递消息给父组件? 本文档使用 书栈(BookStack.CN) 构建 - 26 -
27 .父组件如何更新子组件? 父组件如何更新子组件? props 最简单的也是最常用的父组件更新子组件的方式就是父组件将数据通过props传给子组件,当相关的变 量被更新的时候,MVVM框架会自动将数据的更新映射到视图上。 1. class Son extends san.Component { 2. static template = ` 3. <div> 4. <p>Son's name: {{firstName}}</p> 5. </div> 6. `; 7. }; 8. 9. class Parent extends san.Component { 10. static template = ` 11. <div> 12. <input value="{= firstName =}" placeholder="please input"> 13. <ui-son firstName="{{firstName}}"/> 14. </div> 15. `; 16. 17. static components = { 18. 'ui-son': Son 19. }; 20. 21. initData() { 22. return { 23. firstName: 'trump' 24. } 25. } 26. }; See the Pen san-parent-to-child-prop by liuchaofan (@asd123freedom) on CodePen. ref 更灵活的方式是通过ref拿到子组件的实例,通过这个子组件的实例可以手动调用 this.data.set 来 更新子组件的数据,或者直接调用子组件声明时定义的成员方法。 本文档使用 书栈(BookStack.CN) 构建 - 27 -
28 .父组件如何更新子组件? 1. class Son extends san.Component { 2. static template = ` 3. <div> 4. <p>Son's: {{firstName}}</p> 5. </div> 6. `; 7. }; 8. 9. class Parent extends san.Component { 10. static template = ` 11. <div> 12. <input value="{= firstName =}" placeholder="please input"> 13. <button on-click='onClick'>传给子组件</button> 14. <ui-son san-ref="son"/> 15. </div> 16. `; 17. static components = { 18. 'ui-son': Son 19. }; 20. onClick() { 21. this.ref('son').data.set('firstName', this.data.get('firstName')); 22. } 23. } See the Pen san-parent-to-child-ref by liuchaofan (@asd123freedom) on CodePen. message 除了ref外,父组件在接收子组件向上传递的消息的时候,也可以拿到子组件的实例,之后的操作方式 就和上面所说的一样了。 1. class Son extends san.Component { 2. static template = ` 3. <div> 4. <p>Son's name: {{firstName}}</p> 5. <button on-click='onClick'>I want a name</button> 6. </div> 7. `; 8. 9. onClick() { 10. this.dispatch('son-clicked'); 本文档使用 书栈(BookStack.CN) 构建 - 28 -
29 .父组件如何更新子组件? 11. } 12. }; 13. 14. class Parent extends san.Component { 15. static template = ` 16. <div> 17. <input value="{= firstName =}" placeholder="please input"> 18. <ui-son/> 19. </div> 20. `; 21. 22. // 声明组件要处理的消息 23. static messages = { 24. 'son-clicked': function (arg) { 25. let son = arg.target; 26. let firstName = this.data.get('firstName'); 27. son.data.set('firstName', firstName); 28. } 29. }; 30. 31. static components = { 32. 'ui-son': Son 33. }; 34. 35. initData() { 36. return { 37. firstName: 'trump' 38. } 39. } 40. }; See the Pen san-parent-to-child-prop by liuchaofan (@asd123freedom) on CodePen. 本文档使用 书栈(BookStack.CN) 构建 - 29 -