diff --git a/README.md b/README.md index 3388e83..da10c44 100644 --- a/README.md +++ b/README.md @@ -49,4 +49,4 @@ FineUI 100个问题题,带你走进FineUI的世界 - [26、处理树结构的常用算法]() - [27、组件生命周期与Model状态控制](https://code.fineres.com/projects/BUSSINESS/repos/nuclear-webui/pull-requests/9193/diff/#packages/bi-webui/src/modules/conf/pack/analysis/transfer/operator/dimension/combo/combo.tsx) - [28、几个常用的布局场景]() -- [29、BI.Layers.create参数详解原理]() +- [29、BI.Layers.create参数详解原理](https://code.fanruan.com/dailer/FineUI-100-Questions/src/branch/master/questions/29.BI.Layers.create参数详解原理.md) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js new file mode 100644 index 0000000..11ddb04 --- /dev/null +++ b/docs/.vuepress/config.js @@ -0,0 +1,17 @@ +module.exports = { + title: 'Hello VuePress', + description: 'Just playing around', + themeConfig: { + sidebar:{ + "/guide/":[ + "/guide/", + { + title: 'Guide', + children:[ + "/guide/1", + ] + } + ] + } + } +} diff --git a/docs/README.md b/docs/README.md index 26e97ae..6213276 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1 +1,14 @@ -# Hello VuePress +--- +home: true +heroImage: /logo.jpg +actionText: 快速上手 → +actionLink: /guide/ +features: +- title: 简洁至上 + details: 以 Markdown 为中心的项目结构,以最少的配置帮助你专注于写作。 +- title: Vue驱动 + details: 享受 Vue + webpack 的开发体验,在 Markdown 中使用 Vue 组件,同时可以使用 Vue 来开发自定义主题。 +- title: 高性能 + details: VuePress 为每个页面预渲染生成静态的 HTML,同时在页面被加载的时候,将作为 SPA 运行。 + footer: MIT Licensed | Copyright © 2018-present Evan You +--- diff --git a/docs/guide/1.md b/docs/guide/1.md new file mode 100644 index 0000000..c52814e --- /dev/null +++ b/docs/guide/1.md @@ -0,0 +1 @@ +# helloooo diff --git a/docs/guide/README.md b/docs/guide/README.md new file mode 100644 index 0000000..211462c --- /dev/null +++ b/docs/guide/README.md @@ -0,0 +1,6 @@ +# index + +## index1 + + +## index2 diff --git a/docs/guide/questions/0.前端工作进阶需要哪些必备技能.md b/docs/guide/questions/0.前端工作进阶需要哪些必备技能.md new file mode 100644 index 0000000..630d10f --- /dev/null +++ b/docs/guide/questions/0.前端工作进阶需要哪些必备技能.md @@ -0,0 +1,6 @@ +# 0.前端工作需要哪些必备技能 + +## 1.熟悉各种链工具的使用,包括但不限于webpack,rollup,less,babel,代理配置等 +## 2.了解前后端分离开发模式,熟悉对接流程 +## 3.了解持续集成工具teamcity的基本流程 +## 4.掌握icon-font原理,知晓图标使用方式 diff --git a/docs/guide/questions/1.如何获取当前时间.md b/docs/guide/questions/1.如何获取当前时间.md new file mode 100644 index 0000000..463d1c8 --- /dev/null +++ b/docs/guide/questions/1.如何获取当前时间.md @@ -0,0 +1,19 @@ +# FineUI中如何获取当前时间 + +## 错误写法 + +``` + const date = new Date(); + const time = date.getTime(); +``` + +## 正确写法 + +``` + const time = BI.getTime(); +``` + +## 答案解析 + +首先思考一个场景: 某跨国公司系统管理员人在北京,在系统上配置数据更新时间晚上七点半。可是等到过了七点半,数据还是没有更新。这是为什么呢?原来服务器在伦敦,伦敦时间比北京时间慢7个小时,还没到更新时间呢 +FineUI中BI.getTime方法专门这种场景做了处理,依据服务器时区进行偏移,正确的获取当前时间。 \ No newline at end of file diff --git a/docs/guide/questions/10.BI.config的执行顺序是什么,为什么这么设计.md b/docs/guide/questions/10.BI.config的执行顺序是什么,为什么这么设计.md new file mode 100644 index 0000000..c933a74 --- /dev/null +++ b/docs/guide/questions/10.BI.config的执行顺序是什么,为什么这么设计.md @@ -0,0 +1,83 @@ +# BI.config的执行顺序是什么,为什么这么设计.md + +BI.config配置的方法除了`bi.provider.system`直接执行之外,全都是在获取资源的时候执行 + +对于旧版本的FineUI, config配置的函数是在`BI.init()`中执行的,`BI.init()`又是在什么时候调用的呢,是在第一次`BI.createWidget()`时候调用的. + +这样带来了一个弊端,如果`BI.init()`先被调用了,那么后续`BI.config()`的内容将不再执行 + +例如如下代码,javascript文件加载顺序为abcd,在旧有设计中,d.js获取的items长度是几?答案是1,因为在b.js中调用了`BI.Msg.toast`方法,而这个方法一定会间接的调用到`BI.init()`,这是不可控的,因为我们无法控制开发者要怎么写代码.很多时候出了bug,但是面对茫茫多的插件,完全不知道是由哪个文件引发的. + +``` +// a.js +BI.constant("bi.constant.items", [ + { + id: 1, + text: 1, + }, +]); + +// b.js +BI.Msg.toast("123"); + +// c.js +BI.config("bi.constant.items", items => { + items.push({ + id: 2, + text: 2, + }); + return items; +}); + +// d.js +var items = BI.Constants.getConstant("bi.constant.items"); // 有几个? +``` + +BI.config注册的方法结构是什么样呢? + +``` +interface configFunctions { + [key: string]: Function[]; +} +``` + +`BI.config`的配置函数是如何执行的呢? + +对于每一个资源,都有一个配置函数队列,在每次获取资源的时候执行队列中的配置方法,顺序执行然后清空队列,如果之后再调用`BI.config`配置当前资源,重新讲配置函数加入队列中 + +``` +BI.constant("bi.constant.items", [ + { + id: 1, + text: 1, + }, +]); + +// 此时资源bi.constant.items的配置函数队列中只有一个配置方法等待执行 +BI.config("bi.constant.items", items => { + items.push({ + id: 2, + text: 2, + }); + return items; +}); + +// 获取资源的是执行并清空了队列 +var items = BI.Constants.getConstant("bi.constant.items"); // 2个 + +// 又给资源bi.constant.items的配置函数队列中增加了一个配置方法等待执行 +BI.config("bi.constant.items", items => { + items.push({ + id: 3, + text: 3, + }); + return items; +}); + +// 获取资源的是又执行并清空了队列 +items = BI.Constants.getConstant("bi.constant.items"); // 3个 +``` + +这样设计有什么好处呢? + +首先在获取资源时候才执行配置函数,完全避免了js加载顺序所带来的隐患,其次使配置可以无需立即执行并且可以多次配置. \ No newline at end of file diff --git a/docs/guide/questions/11.在props中处理生命周期函数.md b/docs/guide/questions/11.在props中处理生命周期函数.md new file mode 100644 index 0000000..c3c03e7 --- /dev/null +++ b/docs/guide/questions/11.在props中处理生命周期函数.md @@ -0,0 +1,80 @@ +# 在props中处理生命周期函数 + +在[组件生命周期](http://fanruan.design/doc.html?post=244ba71889)一文中,详细解释了组件中各个生命周期钩子的执行时机.那么是不是我们想要使用生命周期钩子,就一定要写一个组件类呢?答案是否定的. + +类似于[9、高阶组件的render-props](https://code.fanruan.com/dailer/FineUI-100-Questions/src/branch/master/questions/4.高阶组件的render-props.md)文中所讲,render可以通过props指定,那么beforeRender,mounted等其他生命周期函数可否通过props控制呢? 如果和类组件中方法产生冲突,又是如何处理的呢? + +FineUI中有两种处理方式直接覆盖和一并执行 + +直接覆盖的有: beforeInit,beforeRender,render. 如果在props中指定了相应生命周期钩子,则组件类内的方法不会执行 + +一并执行的有: beforeCreate,created,beforeMount,mounted,beforeDestroy,destroyed,beforeUpdate,updated,如果在props中指定了相应生命周期钩子,则按照先执行类方法,再执行props中方法的顺序依次执行 + +如下示例,最终的输出会是什么呢? + +```demo +const logs = []; + +const Menus = BI.inherit(BI.Widget, { + + beforeRender: function () { + logs.push("内部的beforeRender"); + return Promise.resolve(); + }, + + render: function () { + logs.push("内部的render"); + return { + type: "bi.button", + text: "内部的render", + }; + }, + + beforeMount: function () { + logs.push("内部的beforeMount"); + }, + + mounted: function () { + logs.push("内部的beforeMount"); + }, + + setText: function (text) { + this.button.setText(text); + }, +}); + +BI.shortcut("demo.menus", Menus); + +const Widget10 = BI.createWidget({ + type: "bi.vertical", + items: [ + { + el: { + type: "demo.menus", + beforeRender: () => { + logs.push("外面的beforeRender"); + return Promise.resolve(); + }, + render: function () { + logs.push("外面的render"); + return { + type: "bi.label", + ref: (_ref) => { + this.button = _ref; + }, + text: "外面的render", + whiteSpace: "normal", + }; + }, + beforeMount: () => { + logs.push("外面的beforeMount"); + }, + mounted: function () { + logs.push("外面的mounted"); + this.setText(logs.join("\n")); + }, + }, + }, + ], +}); +``` diff --git a/docs/guide/questions/15.如何记录子路由信息并重定向.md b/docs/guide/questions/15.如何记录子路由信息并重定向.md new file mode 100644 index 0000000..3f99d4e --- /dev/null +++ b/docs/guide/questions/15.如何记录子路由信息并重定向.md @@ -0,0 +1,7 @@ +# 如何记录子路由信息并重定向 + +## 业务场景 + +BI6.0公共数据的路由为`analysis`,有若干子路由,如`analysis/table/:id` + +当由`analysis`跳转到`management`,再跳转回来时,这时候进入的是`analysis`根,而不是上一次的子路由,如何解决呢 \ No newline at end of file diff --git a/docs/guide/questions/2.如何格式化输出日期.md b/docs/guide/questions/2.如何格式化输出日期.md new file mode 100644 index 0000000..ccc90a2 --- /dev/null +++ b/docs/guide/questions/2.如何格式化输出日期.md @@ -0,0 +1 @@ +# FineUI中如何格式化输出日期 \ No newline at end of file diff --git a/docs/guide/questions/2.组件的代码设计基本思路.md b/docs/guide/questions/2.组件的代码设计基本思路.md new file mode 100644 index 0000000..c0256d1 --- /dev/null +++ b/docs/guide/questions/2.组件的代码设计基本思路.md @@ -0,0 +1,174 @@ +# 组件的代码设计基本思路 + +我们以一个分页组件为例,阐述一下FineUI中组件代码的基本思路 + +## 控件功能 + +上一页,下一页,带个输入框,有总页数.四个按钮带disabled逻辑,无上一页或者下一页时候灰化 + +![示例](../../../images/9.png) + +## 第一步: 明确对外提供的props + +总页数,当前页数 就这两个属性够了 + +```javascript +const props = { + total: 200, + page: 10, +} +``` + +## 第二步: 明确要对外提供哪些方法 + +一个getValue方法加一个populate就行了 + +```javascript +// 获取当前页码 +const page = this.page.getValue() + +// 设置总页数和页码 +this.pager.attr("total", 100) +this.pager.attr("page", 1) +this.pager.populate() +``` + +## 第三步: 明确对外提供哪些事件 + +事件只需要一个就够了,通知外部使用者当前page,养成良好习惯,事件尽量带上参数 + +```javascript +this.fireEvent("EVENT_CHANGE", page) +``` + +## 生命周期 + +万物皆render,能render一遍就执行好的,尽量别用其他生命周期,mounted之类的,避免没必要的回流重绘 如果组件是异步的, beforeInit/beforeRender.那么你在render中一定可以拿到所有的状态了,就没必要再通过一次watch来实现效果.当然,有些事情必须在mounted中做(例如获取组件宽高)那就另说了. + +## 抛砖引玉 + +比如这个pager组件按钮的灰化,我就借用最新的响应式写个例子 [自动相应变更](http://fanruan.design/doc.html?post=afe4c84120) + +```demo + +const Pager = BI.inherit(BI.Widget, { + props: { + total: 1, + page: 1, + }, + + init: function () { + this.state = Fix.define({ + currentPage: this.options.page, + total: this.options.total, + }); + }, + + render: function () { + + return { + type: "bi.vertical_adapt", + columnSize: [24, 24, "fill", "", 24, 24], + hgap: 5, + items: [ + { + type: "bi.icon_button", + cls: "pre-page-h-font", + disabled: () => { + return this.state.currentPage === 1; + }, + handler: () => { + this.state.currentPage = 1; + this.fireEvent("EVENT_CHANGE", this.getValue()); + }, + }, { + type: "bi.icon_button", + cls: "pre-page-h-font", + disabled: () => { + return this.state.currentPage === 1; + }, + handler: () => { + this.state.currentPage--; + this.fireEvent("EVENT_CHANGE", this.getValue()); + }, + }, { + type: "bi.text_editor", + ref: ref => this.editor = ref, + value: () => this.state.currentPage, + validationChecker: (v) => (BI.parseInt(v) >= 1) && (BI.parseInt(v) <= this.state.total), + errorText: "error", + listeners: [ + { + eventName: "EVENT_CHANGE", + action: () => { + this.state.currentPage = BI.parseInt(this.editor.getValue()); + this.fireEvent("EVENT_CHANGE", this.getValue()); + }, + }, + ], + }, { + type: "bi.label", + text: () => "/" + this.state.total, + }, { + type: "bi.icon_button", + cls: "next-page-h-font", + disabled: () => { + return this.state.currentPage === this.state.total; + }, + handler: () => { + this.state.currentPage++; + this.fireEvent("EVENT_CHANGE", this.getValue()); + }, + }, { + type: "bi.icon_button", + cls: "next-page-h-font", + disabled: () => { + return this.state.currentPage === this.state.total; + }, + handler: () => { + this.state.currentPage = this.state.total; + this.fireEvent("EVENT_CHANGE", this.getValue()); + }, + }, + ], + }; + }, + + getValue: function () { + return this.state.currentPage; + }, + + populate: function () { + this.state.currentPage = this.options.page; + this.state.total = this.options.total; + }, +}); + +BI.shortcut("pager", Pager); + +let pager; + +BI.createWidget({ + type: "bi.vertical", + vgap: 20, + items: [ + { + type: "pager", + ref: ref => pager = ref, + height: 24, + width: 250, + total: 100, + page: 2, + }, { + type: "bi.button", + text: "populate", + handler: () => { + pager.attr("total", 500); + pager.attr("page", 400); + pager.populate(); + }, + }, + ], +}); + +``` diff --git a/docs/guide/questions/20.如何监听元素大小变化.md b/docs/guide/questions/20.如何监听元素大小变化.md new file mode 100644 index 0000000..8d02a85 --- /dev/null +++ b/docs/guide/questions/20.如何监听元素大小变化.md @@ -0,0 +1 @@ +# 如何监听元素大小变化 \ No newline at end of file diff --git a/docs/guide/questions/21.computed进行列表组件的状态控制.md b/docs/guide/questions/21.computed进行列表组件的状态控制.md new file mode 100644 index 0000000..1d61050 --- /dev/null +++ b/docs/guide/questions/21.computed进行列表组件的状态控制.md @@ -0,0 +1,219 @@ +# computed进行列表组件的状态控制 + +开发中我们经常使用computed计算属性控制状态,如列表刷新,列表选中等. +正确理解computed原理并灵活使用 + +看如下示例,把选中状态和列表节点放入同一个computed属性中计算,会导致什么问题呢? + +```js + computed: { + listItems: function () { + return this.model.items.map(item => { + return { + ...item, + type: "bi.text_item", + cls: "bi-list-item-active2", + selected: item.value === this.model.selectedValue, + }; + }); + }, + } +``` + +下面书写了一个简单的demo,可以看到,每次改变selectedValue状态,都会触发列表的重新渲染.当列表节点过多,浏览器性能较差的时候,会出现明显的闪烁或者卡顿. + +究其原因,是因为listItems属性的计算同时依赖items和selectedValue属性,每当selectedValue属性改变都会触发listItems的重新计算,返回值是一个新的数据,因此触发watch,重新populate + +```js +let listModel = BI.inherit(Fix.Model, { + state: function () { + return { + selectedValue: 1, + items: [ + { + text: 1, + value: 1, + }, { + text: 2, + value: 2, + }, { + text: 3, + value: 3, + }, { + text: 4, + value: 4, + }, + ], + }; + }, + + computed: { + listItems: function () { + return this.model.items.map(item => { + return { + ...item, + type: "bi.text_item", + cls: "bi-list-item-active2", + selected: item.value === this.model.selectedValue, + }; + }); + }, + }, + + actions: { + changeSelectedValue: function (value) { + this.model.selectedValue = value; + }, + + addItem: function () { + this.model.items.push({ + text: this.model.items.length + 1, + value: this.model.items.length + 1, + }); + }, + }, +}); + +BI.model("list", listModel); + +let list = BI.inherit(BI.Widget, { + + _store: function () { + return BI.Models.getModel("list"); + }, + + watch: { + listItems: function (items) { + BI.Msg.toast("populate"); + this.list.populate(items); + }, + }, + + render: function () { + return { + type: "bi.vertical", + vgap: 15, + width: 300, + items: [ + { + type: "bi.button_group", + ref: ref => this.list = ref, + layouts: [ + { + type: "bi.vertical", + }, + ], + items: this.model.listItems, + listeners: [ + { + eventName: "EVENT_CHANGE", + action: (value) => { + this.store.changeSelectedValue(value); + }, + }, + ], + }, { + type: "bi.button", + text: "选中下一个节点", + handler: () => { + this.store.changeSelectedValue(this.model.selectedValue + 1); + }, + }, { + type: "bi.button", + text: "添加一个节点", + handler: () => { + this.store.addItem(); + }, + }, + ], + + }; + }, +}); + +BI.shortcut("list", list); + + +const ListWidget = BI.createWidget({ + type: "bi.vertical", + items: [ + { + type: "list", + }, + ], +}); + +return ListWidget; +``` + +![示例](../../../images/20.gif) + +要如何避免这类问题呢?其实就一句话,将选中状态与列表节点状态分离开.在view层进行控件层面的属性控制 + +例如,computed属性中不再控制选中状态,改为单独watch选中状态,调用组件的setValue api 设置选中状态,此时两个状态分离,仅改变选中状态不会触发列表的重新渲染 + +```js + computed: { + listItems: function () { + return this.model.items.map(item => { + return { + ...item, + type: "bi.text_item", + cls: "bi-list-item-active2", + // selected: item.value === this.model.selectedValue, + }; + }); + }, + } + + watch: { + selectedValue: function (value) { + this.list.setValue(value); + }, + + listItems: function (items) { + BI.Msg.toast("populate"); + this.list.populate(items); + }, + }, +``` + + +为了列表重新渲染之后依旧保持选中值,一般有两种处理方式 +1. populate之后调用 setValue +2. 在populate之前进行一次遍历,设置selected属性 + +```js + watch: { + listItems: function (items) { + this.list.populate(items); + this.list.setValue(this.model.selectedValue); + }, + }, +``` + +```js + watch: { + listItems: function (items) { + this.list.populate(items.map(item => ({ ...item, selected: item.value === this.model.selectedValue }))); + }, + }, +``` + + +利用响应式新特性,可以更加逻辑清晰的进行选中和列表渲染控制 +不过响应式并不能解决populate之后状态保持问题,因此需要在populate之前额外设置selected属性 + +```js + { + type: "bi.button_group", + ref: ref => this.list = ref, + layouts: [ + { + type: "bi.vertical", + }, + ], + items: () => this.model.listItems, + value: () => this.model.selectedValue, + }, +``` diff --git a/docs/guide/questions/28.几个常用的布局场景.md b/docs/guide/questions/28.几个常用的布局场景.md new file mode 100644 index 0000000..b36c8ff --- /dev/null +++ b/docs/guide/questions/28.几个常用的布局场景.md @@ -0,0 +1,87 @@ +# 几个常用的布局场景 + +## 1.上对齐的水平布局如何对齐? + +举一个反面例子 + +![示例1](../../../images/26.png) +![示例2](../../../images/27.png) + + +缓存配置页面,一个水平方向布局,左侧是配置项名称,右侧是具体的配置细节. 可以很明显的看出,左右两段的水平并没有对齐 + +如何通过布局来实现齐平呢 + +一般情况下,只需要让左右两侧的元素保持相同高度即可 + +```js +const widget = { + type: "bi.horizontal", + verticalAlign: "top", + columnSize: [100, ""], + items: [ + { + type: "bi.label", + height: 36, + text: "全局缓存策略", + }, { + type: "bi.vertical", + items: [ + { + type: "bi.single_select_radio_item", + height: 36, + text: "radio1", + }, { + type: "bi.single_select_radio_item", + height: 36, + text: "radio2", + }, + ], + }, + ], +}; +``` + +![示例3](../../../images/28.png) + +如果是文本段落需要对齐的场景,对文本段落设置相应的行高即可.对于FineUI中label组件来说即为`textHeight`属性. FineUI中toast组件的布局方式就是如此,通过控制`textHeight` +使得icon与文字对齐 + +```js +const widget = { + type: "bi.horizontal", + width: 300, + scrollx: false, + verticalAlign: "top", + columnSize: [100, "fill"], + items: [ + { + type: "bi.label", + height: 36, + text: "全局缓存策略", + }, { + type: "bi.vertical", + items: [ + { + type: "bi.label", + whiteSpace: "normal", + textHeight: 36, + text: "S2 定位是一个轻量级的表格渲染核心,所以没有 Excel/SpreadJS 那样复杂的功能和数据模型。在大数据表格组件这个场景下,其实不需要那么重的数据模型,S2 的轻量级设计是更合适的。在性能表现上,10 个Tab 切换时,切换耗时为 80ms,是老方案的 6倍。在 5 万条数据的情况下,S2 渲染和初始化只需要 1秒。是老方案的 8 倍。\n", + }, + ], + }, + ], +}; +``` + +![示例4](../../../images/30.png) + +## 2. 高度自适应的tab如何使用 + +## 3. 让滚动条贴附在容器边缘的技巧 + +日常业务中会出现滚动条,如果没有贴边显示的话,视觉效果会大打折扣.直接原因是一层又一层的布局嵌套,使得当前滚动的容器自身就已经脱离了页面区域. +如何在不借助`padding`的情况下,让滚动条贴附在容器边缘呢? + +![示例5](../../../images/31.png) +![示例6](../../../images/32.png) diff --git a/docs/guide/questions/3.为什么传递时间信息时候推荐使用时间戳.md b/docs/guide/questions/3.为什么传递时间信息时候推荐使用时间戳.md new file mode 100644 index 0000000..71181bf --- /dev/null +++ b/docs/guide/questions/3.为什么传递时间信息时候推荐使用时间戳.md @@ -0,0 +1,40 @@ +# 为什么传递时间信息时候推荐使用时间戳 + +前后端传递时间信息的时候,推荐使用标准时间戳,即自1970-1-1 00:00:00 UTC(世界标准时间)至今所经过的毫秒数。为什么? + +## 错误写法 + +``` + // 前端向后端传递时间 + const time = "2022-04-19 10:56:05" + // 前端获取到后端传递的时间信息进行解析 + const timeStr = "2022-04-19 10:56:05" + const time = BI.getDate(timeStr).getTime() // 期望输出:1650336965000 + console.log(`距离现在经过了${(BI.getTime() - time) / 1000}秒`) +``` + +## 正确写法 + +``` + // 前端向后端传递时间 + const time = BI.getTime() + // 前端获取到后端传递的时间信息进行解析 + const time = 1650336965000 // 直接接收时间戳 + console.log(`距离现在经过了${(BI.getTime() - time) / 1000}秒`) +``` + +## 答案解析 + +在[MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)中关于Date对象的构造函数描述中,特别强调了**由于浏览器之间的差异与不一致性,强烈不推荐使用Date构造函数来解析日期字符串**,这是为什么呢,如果把上面的错误示例放到IE浏览器中执行 + +``` + const timeStr = "2022-04-19 10:56:05" + const time = BI.getDate(timeStr).getTime() // 会收到报错:Invalid DateDate +``` + +如果一定要解析时间字符串,FineUI中提供了`BI.parseDateTime`方法,仅适用于处理一些用户手动输入的时间格式字符串场景 + +``` + const timeStr = "2022-04-19 10:56:05" + const time = BI.getDate(BI.parseDateTime(timeStr,"%Y-%X-%d %H:%M:%S")).getTime() // 期望输出:1650336965000 +``` \ No newline at end of file diff --git a/docs/guide/questions/3.关于组件引用的奥秘,ref知多少.md b/docs/guide/questions/3.关于组件引用的奥秘,ref知多少.md new file mode 100644 index 0000000..26c1a8a --- /dev/null +++ b/docs/guide/questions/3.关于组件引用的奥秘,ref知多少.md @@ -0,0 +1,97 @@ +# 关于组件引用的奥秘,ref知多少 + +类似于React和Vue获取DOM元素引用的形式FineUI中也可以获取组件实例的引用,有两种方式 + +1. BI.createWidget方法的返回值 +2. 组件props中的ref属性(推荐) + +## ref在什么时候调用 + +组件ref会在组件render之后回调`this.options.ref.call(this, this)`,同时也会在组件destoryed之后再次回调`this.options.ref.call(null, null)` + +这也是为什么在[combo的一些特性详解](./80.combo的一些特性详解.md)一文中强调combo的popup是在弹出时候创建的,有时候虽然写了ref,但是暂时还拿不到. + +## ref不是只回调一次哦 + +使用ref时推荐如下写法,使用参数 +``` +{ + type: "bi.button", + text: "左上角", + ref: function (_ref) { + xxx = _ref; + }, +} +``` +不推荐用this赋值的写法,因为在组件destoryed之后的回调,this可能是window对象. +``` +{ + type: "bi.button", + text: "左上角", + ref: function () { + xxx = this; + }, +} +``` + +## 用BI.createWidget有什么不好吗,什么时候可以用BI.createWidget呢 + +1. 相较于ref,BI.createWidget缺少相应的内存回收 +2. BI.createWidget会破坏render()=>json 的清晰代码结构 +3. 在封装高阶组件的时候,组件内部需要获取子组件引用,同时还有外部传入的props存在ref的场景. + +例如如下示例,此时外部传入的ref失效了 +``` +class MyComponent extends BI.Widget { + static xtype = "my.component"; + + render() { + + const el = this.options.el; + + return { + type: "bi.combo", + el: { + ...el, + ref: ref => { + this.trigger = ref; + }, + }, + popup: {}, + }; + } +} + +let myRef; +const widget = { + type: MyComponent.xtype, + ref: ref => { + myRef = ref; + }, +}; render: +} +``` + +如何避免这类错误呢,有两种可选方案,使用BI.createWidget方法,或者使用私有的__ref + +``` +class MyComponent extends BI.Widget { + static xtype = "my.component"; + + render() { + + const el = this.options.el; + + return { + type: "bi.combo", + el: { + ...el, + __ref: ref => { + this.trigger = ref; + }, + }, + popup: {}, + }; + } +} +``` \ No newline at end of file diff --git a/docs/guide/questions/4.使用ButtonGroup控制可点击组件的控制选中状态.md b/docs/guide/questions/4.使用ButtonGroup控制可点击组件的控制选中状态.md new file mode 100644 index 0000000..4c2a2e0 --- /dev/null +++ b/docs/guide/questions/4.使用ButtonGroup控制可点击组件的控制选中状态.md @@ -0,0 +1,115 @@ +# 使用ButtonGroup控制可点击组件的控制选中状态 + +先看如下示例,被选中的radio,再次点击,会使其取消选中,大多数时候这不符合我们的预期 +```demo +{ + type: "bi.vertical_adapt", + height: 30, + scrollable: false, + items: [ + { + type: "bi.single_select_radio_item", + text: "选项1", + }, { + type: "bi.single_select_radio_item", + text: "选项2", + }, + ], +} +``` + +![示例](../../../images/13.gif) + +为了实现真正的单选效果,我们需要进行状态控制,尽管如此,选中状态还是会跳来跳去,如果选项比较多的话将是个灾难 + +```demo +const radios = []; + +const widget = BI.createWidget({ + type: "bi.vertical_adapt", + height: 30, + scrollable: false, + items: [ + { + type: "bi.single_select_radio_item", + ref: function (_ref) { + radios[0] = _ref; + }, + text: "选项1", + handler: () => { + radios[1].setSelected(!radios[0].isSelected()); + }, + }, { + type: "bi.single_select_radio_item", + ref: function (_ref) { + radios[1] = _ref; + }, + text: "选项2", + handler: () => { + radios[0].setSelected(!radios[1].isSelected()); + }, + }, + ], +}); +``` + +![示例](../../../images/14.gif) + +FineUI中提供了ButtonGroup控件,名字叫逻辑列表.他内部封装了选中逻辑控制,简单几行代码,就可以实现一个选项切换 + +```demo +const widget = BI.createWidget({ + type: "bi.button_group", + height: 30, + layouts: { + type: "bi.vertical_adapt", + }, + items: [ + { + type: "bi.single_select_radio_item", + text: "选项1", + value: 1, + }, { + type: "bi.single_select_radio_item", + text: "选项2", + value: 2, + }, + ], +}); +``` + +![示例](../../../images/15.gif) + +ButtonGroup组件的默认chooseType属性为单选,也可以实现多选 + +```demo +const widget = BI.createWidget({ + type: "bi.button_group", + height: 30, + layouts: { + type: "bi.vertical_adapt", + }, + chooseType: BI.ButtonGroup.CHOOSE_TYPE_MULTI, + items: [ + { + type: "bi.multi_select_item", + text: "选项1", + value: 1, + }, { + type: "bi.multi_select_item", + text: "选项2", + value: 2, + }, { + type: "bi.multi_select_item", + text: "选项3", + value: 3, + }, { + type: "bi.multi_select_item", + text: "选项4", + value: 4, + }, + ], +}); +``` + +![示例](../../../images/16.gif) diff --git a/docs/guide/questions/4.高阶组件的render-props.md b/docs/guide/questions/4.高阶组件的render-props.md new file mode 100644 index 0000000..f678f03 --- /dev/null +++ b/docs/guide/questions/4.高阶组件的render-props.md @@ -0,0 +1,58 @@ +## 高阶组件的render props.md + +在FineUI中,我们经常见到一些继承抽象组件的写法,为了实现某种功能,继承抽象组件重新定义一个组件 + +依据我们的编码规范,一个组件一个文件,被迫新建众多文件 +``` +// 用于loading效果的组件 +const Widget1 = BI.inherit(BI.Pane, { + + beforeRender: function () { + this.loading(); + }, + +}); + +// 用于封装可以点击的组件 +const Widget2 = BI.inherit(BI.BasicButton, { + + render: function () { + + }, + +}); + +// 用于封装带有tooltip功能的组件 +const Widget3 = BI.inherit(BI.Single, { + + render: function () { + + }, + +}); +``` + +以BI.BasicButton举例,很多时候我们只是想快速创建一个可以点击的组件,此时可以借助render-props来实现 +``` +const MyButton = { + type: "bi.basic_button", + render: () => { + return { + type: "bi.vertical_adapt", + items: [ + { + type: "bi.label", + text: "文字", + }, { + type: "bi.icon_button", + cls: "delete-font", + title: "删除按钮", + }, + ], + }; + }, + handler: () => { + console.log("点击了"); + }, +}; +``` \ No newline at end of file diff --git a/docs/guide/questions/40.我们为什么要设计el这个属性.md b/docs/guide/questions/40.我们为什么要设计el这个属性.md new file mode 100644 index 0000000..95b1acc --- /dev/null +++ b/docs/guide/questions/40.我们为什么要设计el这个属性.md @@ -0,0 +1,51 @@ +# 我们为什么要设计el这个属性 + +我们在使用布局组件的时候,有时候会在item中直接写lgap,hgap,vgap之类的属性,通常最终的页面结果是符合我们的预期的,但是当items中的子组件同时接收lgap作为props的时候,结果就变了,通常会出现间距变成两倍了的情况 + +``` +BI.createWidget({ + type: "bi.left", + items: [ + { + type: "bi.label", + lgap: 10, + }, + ], +}); +``` + +![示例](../../../images/3.png) +![示例](../../../images/4.png) + +出现这种情况是因为布局组件会根据itemw中元素的gap属性设置margin值,而子组件也会读取gap属性在组件内部逻辑值使用这个值,因此出现了双倍间距. + +采用el做隔离,可以避免这种情况,这时候子组件bi.label并没有lgap属性,但是布局组件依然可以正确获取items子元素的lgap属性 + +``` +BI.createWidget({ + type: "bi.left", + items: [ + { + el: { + type: "bi.label", + text: "测试", + }, + lgap: 10, + }, + ], +}); +``` + +为了简化写法,FineUI新增了下划线+gap属性,如_hgap,_vgap,_lgap等,同样起到了隔离作用 + +``` +BI.createWidget({ + type: "bi.left", + items: [ + { + type: "bi.label", + _lgap: 10, + }, + ], +}); +``` diff --git a/docs/guide/questions/41.绝对布局的隐藏知识点.md b/docs/guide/questions/41.绝对布局的隐藏知识点.md new file mode 100644 index 0000000..157c1dd --- /dev/null +++ b/docs/guide/questions/41.绝对布局的隐藏知识点.md @@ -0,0 +1,184 @@ +# 绝对布局的隐藏知识点 + +bi.absolute布局可以说是在flex类布局没有大规模应用之前,解决布局问题的好伙伴 + +下面列举一下绝对布局的特性和在开发中可以解决的问题场景 + +1. 绝对布局子元素位置与宽高的影响 + +对于绝对布局的子元素来说,如果还设置了width,且同时设置了left/right属性,那么仅有left生效.同理如果还设置了height,且top/bottom同时存在时top生效. + +```demo +BI.createWidget({ + type: "bi.absolute", + width: 200, + height: 200, + items: [ + { + el: { + type: "bi.layout", + css: { + background: "blue", + }, + }, + inset: 0, + }, { + el: { + type: "bi.layout", + css: { + background: "red", + }, + width: 24, + height: 24, + }, + inset: 0, + }, + ], +}); +``` + +如示例所示,虽然设置了right和bottom为0,但实际并未生效 + +![示例](../../../images/5.png) + +2. 可以利用绝对布局实现拉伸效果 + +如果想让子元素撑满父元素,可以利用绝对布局设置位置值来实现 + +```demo +BI.createWidget({ + type: "bi.absolute", + width: 300, + height: 300, + css: { + background: "blue", + }, + items: [ + { + el: { + type: "bi.layout", + css: { + background: "red", + }, + }, + inset: 10, + }, + ], +}); +``` + +![示例](../../../images/6.png) + +3. 绝对布局也有隐藏的z-index层级,排在后面的层级高 + +利用这种特性,我们可以实现一些元素覆盖在下层元素的场景 + +```demo +BI.createWidget({ + type: "bi.absolute", + width: 200, + height: 200, + items: [ + { + el: { + type: "bi.layout", + css: { + background: "blue", + }, + }, + inset: 0, + }, { + el: { + type: "bi.layout", + css: { + background: "red", + }, + width: 24, + height: 24, + }, + right: 10, + bottom: 10, + }, + ], +}); +``` + +![示例](../../../images/7.png) + +4. 位置属性使用负值会有起效 + +在开发过程中会遇到有些按钮图标为了位置美观,会脱离正常位置的情况,此时利用负值属性可以很轻松实现 + +```demo +BI.createWidget({ + type: "bi.absolute", + width: 300, + height: 300, + css: { + background: "red", + }, + items: [ + { + el: { + type: "bi.button", + text: "保存", + }, + top: -30, + right: 10, + }, + ], +}); +``` + +![示例](../../../images/8.png) + +5. 绝对布局支持shorthand写法inset + +使用Chrome审查元素的同学可能发现了,绝对布局的元素在chrome中会显示`inset: 0`这种形式,实际上这是top right bottom left属性的简写,规则遵循顺时针方向.FineUI同样支持了这种写法,需要注意的是只有第一种情况支持属性名为数字,其余情况需要加引号,毕竟我们是写js而不是css + +``` +inset: 10 /* value applied to all edges */ +inset: "4 8" /* top/bottom left/right */ +inset: "5 15 10" /* top left/right bottom */ +inset: "2 3 3 3" /* top right bottom left */ +``` + +6. 绝对布局可以和其他布局共存 + +FineUI中组件render方法支持return对象或者数组,可以利用绝对布局几乎没有副作用的特点,附加一些内容 + +``` +BI.createWidget({ + type: "bi.basic_button", + width: 200, + height: 200, + cls: "bi-border", + render: () => { + return [ + { + type: "bi.center_adapt", + items: [ + { + type: "bi.button", + text: "居中", + }, + ], + }, { + type: "bi.absolute", + items: [ + { + el: { + type: "bi.button", + text: "左上角", + }, + top: 10, + right: 10, + }, + ], + }, + ]; + }, +}); +``` + +![示例](../../../images/10.png) diff --git a/docs/guide/questions/42.何时会滚动,滚动条位置如何贴边,究竟是怎么回事.md b/docs/guide/questions/42.何时会滚动,滚动条位置如何贴边,究竟是怎么回事.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/questions/43.利用响应式编写组件代码的新思路.md b/docs/guide/questions/43.利用响应式编写组件代码的新思路.md new file mode 100644 index 0000000..dc4f8e3 --- /dev/null +++ b/docs/guide/questions/43.利用响应式编写组件代码的新思路.md @@ -0,0 +1,167 @@ +有了响应式,我们在开发组件的时候也可以体验到数据双向绑定.省去了进行状态控制需要额外定义store的流程 + +类似于Fix.Model, 我们约定state属性用来保存状态,在beforeCreate生命周期中定义. + +```js + +const Pager = BI.inherit(BI.Widget, { + props: { + total: 1, + page: 1, + }, + + beforeCreate: function () { + this.state = Fix.define({ + currentPage: this.options.page, + total: this.options.total, + }); + }, + + render: function () { + + return { + type: "bi.vertical_adapt", + columnSize: [24, 24, "fill", "", 24, 24], + hgap: 5, + items: [ + { + type: "bi.icon_button", + cls: "pre-page-h-font", + disabled: () => { + return this.state.currentPage === 1; + }, + handler: () => { + this.state.currentPage = 1; + this.fireEvent("EVENT_CHANGE", this.getValue()); + }, + }, { + type: "bi.icon_button", + cls: "pre-page-h-font", + disabled: () => { + return this.state.currentPage === 1; + }, + handler: () => { + this.state.currentPage--; + this.fireEvent("EVENT_CHANGE", this.getValue()); + }, + }, { + type: "bi.text_editor", + ref: ref => this.editor = ref, + value: () => this.state.currentPage, + validationChecker: (v) => (BI.parseInt(v) >= 1) && (BI.parseInt(v) <= this.state.total), + errorText: "error", + listeners: [ + { + eventName: "EVENT_CHANGE", + action: () => { + this.state.currentPage = BI.parseInt(this.editor.getValue()); + this.fireEvent("EVENT_CHANGE", this.getValue()); + }, + }, + ], + }, { + type: "bi.label", + text: () => "/" + this.state.total, + }, { + type: "bi.icon_button", + cls: "next-page-h-font", + disabled: () => { + return this.state.currentPage === this.state.total; + }, + handler: () => { + this.state.currentPage++; + this.fireEvent("EVENT_CHANGE", this.getValue()); + }, + }, { + type: "bi.icon_button", + cls: "next-page-h-font", + disabled: () => { + return this.state.currentPage === this.state.total; + }, + handler: () => { + this.state.currentPage = this.state.total; + this.fireEvent("EVENT_CHANGE", this.getValue()); + }, + }, + ], + }; + }, + + getValue: function () { + return this.state.currentPage; + }, + + populate: function () { + this.state.currentPage = this.options.page; + this.state.total = this.options.total; + }, +}); + +BI.shortcut("pager", Pager); + +let pager; + +BI.createWidget({ + type: "bi.vertical", + vgap: 20, + items: [ + { + type: "pager", + ref: ref => pager = ref, + height: 24, + width: 250, + total: 100, + page: 2, + }, { + type: "bi.button", + text: "populate", + handler: () => { + pager.attr("total", 500); + pager.attr("page", 400); + pager.populate(); + }, + }, + ], +}); + +``` + +如果使用的是es6或者typescript开发,那么可以直接定义类的state属性 + +```javascript +import { shortcut } from "@core"; + +@shortcut() +export class Steps extends BI.Widget { + + static xtype = "steps"; + + props = { + current: 1, + }; + + state = Fix.define({ + current: 1, + }); + + beforeCreate() { + this.state.current = this.options.current; + } + + render() { + return { + type: "bi.label", + text: () => `第${this.state.current}步`, + }; + } + + setValue(step) { + return this.state.current = step; + } + + getValue() { + return this.state.current; + } +} + +``` \ No newline at end of file diff --git a/docs/guide/questions/44.如何提供异步配置的接口.md b/docs/guide/questions/44.如何提供异步配置的接口.md new file mode 100644 index 0000000..69285e4 --- /dev/null +++ b/docs/guide/questions/44.如何提供异步配置的接口.md @@ -0,0 +1,150 @@ +# 如何提供异步配置的接口 + +回顾一下我们常用的提供拓展接口方式 + +1. 定义privider +2. 在被拓展的组件中调用`BI.Providers.getProvider().xxx` +3. 开发者通过`BI.config(xxx,privider => provider.inject())`注册 + +我们提供的配置接口,绝大多数时候都要求BI.config 要在资源加载前执行 +但是有些时候,插件资源是异步加载到,配置代码要异步执行,该如何设计接口? + +![示例](../../../images/18.gif) + +这里分享一个利用响应式特性开放异步接口的方式,核心思路是将拓展数据定义为响应式的,然后组件层采用响应式写法 + + +首先我们在Provider中定义响应式数据 + +```js +const Provider = function () { + + const state = Fix.define({ + menus: [], + }); + + this.inject = function (menu) { + state.menus.push(menu); + }; + + this.$get = function () { + return BI.inherit(BI.OB, { + getMenus: () => { + return state.menus.slice(0); + }, + }); + }; + +}; +BI.provider("demo.provider.menus", Provider); +``` + + +之后再组件中通过函数响应式使用provider提供的数据即可 +```js +const Menus = function () { + + const getMenuItems = function () { + return BI.Providers.getProvider("demo.provider.menus").getMenus(); + }; + + const defaultMenus = [ + { + type: "bi.button", + text: "第一个", + }, { + type: "bi.button", + text: "第二个", + }, + ]; + + return { + type: "bi.vertical", + items: () => BI.concat(defaultMenus, getMenuItems()), + }; +}; +``` + +开发者依然通过同步的方式调用`BI.config`,但是配置函数却可以是异步的 +(思考一下为什么不是`setTimout(() => BI.config()`这种写法) +```js +BI.config("demo.provider.menus", provider => { + setTimeout(() => { + provider.inject({ + type: "bi.button", + text: "第三个", + }); + }, 4000); +}); +``` + +最终的效果呈现 + +![示例](../../../images/19.gif) + +```demo +const Provider = function () { + + const state = Fix.define({ + menus: [], + }); + + this.inject = function (menu) { + state.menus.push(menu); + }; + + this.$get = function () { + return BI.inherit(BI.OB, { + getMenus: () => { + return state.menus.slice(0); + }, + }); + }; + +}; +BI.provider("demo.provider.menus", Provider); + +const Menus = function () { + + const getMenuItems = function () { + return BI.Providers.getProvider("demo.provider.menus").getMenus(); + }; + + const defaultMenus = [ + { + type: "bi.button", + text: "第一个", + }, { + type: "bi.button", + text: "第二个", + }, + ]; + + return { + type: "bi.vertical", + items: () => BI.concat(defaultMenus, getMenuItems()), + }; +}; + +BI.config("demo.provider.menus", provider => { + setTimeout(() => { + provider.inject({ + type: "bi.button", + text: "第三个", + }); + }, 4000); +}); + + +const Widget10 = BI.createWidget({ + type: "bi.vertical", + items: [ + { + el: Menus(), + }, + ], +}); + +return Widget10; + +``` diff --git a/docs/guide/questions/50.前端如何正确书写资源路径.md b/docs/guide/questions/50.前端如何正确书写资源路径.md new file mode 100644 index 0000000..ebbb7a2 --- /dev/null +++ b/docs/guide/questions/50.前端如何正确书写资源路径.md @@ -0,0 +1,21 @@ +# 前端如何正确书写资源路径 + +项目开发中,字体,图片,脚本等内容都涉及到资源加载,FineUI中在项目中默认的资源路径是如下结构 + +``` +@webUrl: 'resources?path=/com/fr/web/ui/'; + +@fontUrl: '@{webUrl}font/'; //图片的基本地址 +@imageUrl: '@{webUrl}images/1x/'; //图片的基本地址 +@image2xUrl: '@{webUrl}images/2x/'; //2倍图片的基本地址 +``` + +如果想要使用自己版本的字体文件或图片,可以通过在项目中定义less变量来实现 + +``` +@webUrl: 'resources?path=/com/fr/plugin/xxxweb/ui/'; + +@fontUrl: '@{webUrl}font/'; +@imageUrl: '@{webUrl}images/1x/'; +@image2xUrl: '@{webUrl}images/2x/'; +``` \ No newline at end of file diff --git a/docs/guide/questions/55.Fix中对于对象属性的监听,为什么要先定义属性才可以正常watch,如何解决.md b/docs/guide/questions/55.Fix中对于对象属性的监听,为什么要先定义属性才可以正常watch,如何解决.md new file mode 100644 index 0000000..b52475a --- /dev/null +++ b/docs/guide/questions/55.Fix中对于对象属性的监听,为什么要先定义属性才可以正常watch,如何解决.md @@ -0,0 +1,55 @@ +# Fix中对于对象属性的监听,为什么要先定义属性才可以正常watch,如何解决? + + +先看如下代码示例,很明显按钮被点击后并不会输出phone的值,其实这和Vue2类似,是因为Fix在响应式处理的时候,遍历对象的每一个属性添加依赖的,并不能检测到新增的属性. + +```javascript +const state = Fix.define({ + name: "小明", + details: { + age: 18, + address: "北京", + }, +}); + +Fix.watch(state, "details.phone", phone => { + console.log(phone); +}); + + +const widget = BI.createWidget({ + type: "bi.button", + text: "button", + handler: () => { + state.details.phone = "123456789"; + }, +}); +``` + +在日常实践中我们一般采用两种方式来处理这种情况 + +1. 修改整个对象的引用 + +``` +const widget = BI.createWidget({ + type: "bi.button", + text: "button", + handler: () => { + state.details = { ...state.details, phone: "123456789"}; + }, +}); +``` + +2. 使用Fix.set方法 + +类似vue的$set方法 + +``` +const widget = BI.createWidget({ + type: "bi.button", + text: "button", + handler: () => { + Fix.set(state.details, "phone", "123456789"); + }, +}); +``` \ No newline at end of file diff --git a/docs/guide/questions/55.watch使用的常见误区.md b/docs/guide/questions/55.watch使用的常见误区.md new file mode 100644 index 0000000..fc2d58d --- /dev/null +++ b/docs/guide/questions/55.watch使用的常见误区.md @@ -0,0 +1,7 @@ +# watch使用的常见误区 + +示例 + +https://code.fineres.com/projects/DEC/repos/decision-webui/pull-requests/7973/diff#src/modules/management/authority/carrierdimenision/entities/system/system.authority.js + +![示例](../../../images/1.png) diff --git a/docs/guide/questions/80.combo的一些特性详解.md b/docs/guide/questions/80.combo的一些特性详解.md new file mode 100644 index 0000000..719e35c --- /dev/null +++ b/docs/guide/questions/80.combo的一些特性详解.md @@ -0,0 +1,245 @@ +# combo一些不为人知的特性 + +## combo的trigger属性是可以设置为空的 + +去掉combo的默认弹出事件,手动控制showView,在某些场景中很有用.例如需要控制有些场景要弹出,有些场景不要弹出. + +例如BI业务中有一个场景,叫做"开启权限继承后气泡提示" + +![示例](../../../images/17.gif) + +```demo +let combo; +let switchBtn; + +BI.createWidget({ + type: "bi.bubble_combo", + ref: (ref) => combo = ref, + el: { + type: "bi.switch", + ref: (ref) => switchBtn = ref, + handler: () => { + switchBtn.isSelected() ? combo.showView() : combo.hideView(); + }, + }, + trigger: "", + popup: { + el: { + type: "bi.label", + text: "提示文字", + }, + }, +}); +``` + +另一个场景,有些提示文本提示一次后不再提示, + +```demo + +let combo; +BI.createWidget({ + type: "bi.vertical", + items: [ + { + type: "bi.combo", + ref: ref => combo = ref, + trigger: "", + el: { + type: "bi.button", + text: "触发弹出", + handler: () => { + if (BI.Cache.getItem("confirmed")) { + combo.showView(); + return; + } + BI.Msg.confirm("弹出是手动的,你知道吧", confirmed => { + if (confirmed) { + BI.Cache.setItem("confirmed", true); + combo.showView(); + } + }); + }, + }, + popup: { + el: { + type: "bi.label", + text: "popup", + }, + }, + }, + ], +}); + +``` + +## combo的popup是在弹出时候创建的,但是props却是在combo创建时候确定的 + +应对这种情况,常见的解决方案是`isDefaultInit: true`或者将popup属性改为函数 +注意设置了`isDefaultInit: true`后会有响应的性能或体验损失 + +```demo + +BI.createWidget({ + type: "bi.vertical", + items: [ + { + type: "bi.combo", + el: { + type: "bi.label", + text: BI.getDate(), + }, + popup: { + el: { + type: "bi.label", + text: BI.getDate(), + }, + }, + }, { + type: "bi.combo", + el: { + type: "bi.label", + text: BI.getDate(), + }, + isDefaultInit: true, + popup: { + el: { + type: "bi.label", + text: BI.getDate(), + }, + }, + }, { + type: "bi.combo", + el: { + type: "bi.label", + text: BI.getDate(), + }, + popup: () => ({ + el: { + type: "bi.label", + text: BI.getDate(), + }, + }), + }, + ], +}); + +``` + +## combo的value传递和getValue很奇怪 + +在多数场景中,给bi.combo传递value属性,可以使下拉框在展开时候自动选中某一项.前提是popup的el组件支持value属性 +如下的combo,value是不生效的.需要对bi.button_group传递value属性 +同样的,如果在展开之前value可能发生改变,要注意value的同步 + +```demo + +var Combo = BI.inherit(BI.Widget, { + + render: function () { + return { + type: "bi.combo", + el: { + type: "bi.label", + }, + ref: ref => this.combo = ref, + value: this.options.value, + popup: { + el: { + type: "bi.vertical", + items: [ + { + type: "bi.button_group", + layouts: [ + { + type: "bi.vertical", + }, + ], + items: [ + { + type: "bi.single_select_radio_item", + text: "1", + value: 1, + }, { + type: "bi.single_select_radio_item", + text: "2", + value: 2, + }, + ], + }, + ], + }, + }, + }; + }, + + getValue: function () { + return this.combo.getValue(); + }, + + setValue: function (v) { + this.combo.setValue(v); + }, +}); + +BI.shortcut("demo.combo", Combo); + +``` + +## destroyWhenHide + +当控制下拉框的状态比较复杂的时候,可以通过收起时候销毁再重新创建来简化 +例如消息下拉框,内部两个独立的业务组件,一个展示消息列表,一个展示二维码.希望每次弹出都能重新加载消息.配置destroyWhenHide可以完全交友message_list +的生命周期去控制,避免了监听combo弹出事件,在命令式调用刷新逻辑. + +```demo + +BI.createWidget({ + type: "bi.vertical", + items: [ + { + type: "bi.combo", + ref: ref => combo = ref, + trigger: "", + el: { + type: "bi.button", + text: "触发弹出", + }, + destroyWhenHide: true, + popup: { + el: { + type: "bi.vertical", + items: [ + { + type: "message_list", + }, { + type: "qr_code", + }, + ], + }, + }, + listeners: [ + { + eventName: BI.Combo.EVENT_AFTER_POPUPVIEW, + action: function () { + // do something + }, + }, + ], + }, + ], +}); + +``` + + +## hideWhenBlur + +先说怎么屏蔽这个行为,再说为什么设计这个属性 + +1. 全局变量`BI.EVENT_BLUR`改为`false` +2. combo配置`hideWhenBlur: false` + +为什么要添加这一个特性呢,实际上是因为iframe会隔离事件的缘故,下拉框弹出后,鼠标在iframe中操作并不会触发外部的`document.mousedown` +因此下拉框也就无法收起了. +以前是怎么处理呢,`trigger: "click-hover"`,即点击触发弹出,鼠标移除触发收起,但是体验差点意思 +![](../../../images/2.png) diff --git a/docs/guide/questions/9.BI.config都可以做那些事情.md b/docs/guide/questions/9.BI.config都可以做那些事情.md new file mode 100644 index 0000000..d33a6de --- /dev/null +++ b/docs/guide/questions/9.BI.config都可以做那些事情.md @@ -0,0 +1,86 @@ +# BI.config都可以做哪些事情,有哪些应用场景? + +1. 修改组件的props + +``` +// 本示例修改了组件的type属性,也是常用的一种场景,讲一个组件替换为另外一个 +BI.config("bi.button", props => { + props.type = "my.button"; + return props; +}); +``` + +2. 修改组件的默认props + +``` +// 本示例修改了bi.button的默认属性,配置方法是在组件的shortcut后面添加.props +BI.config("bi.button.props", props => { + props.minWidth = 100; + return props; +}); +``` + +3. 修改常量资源 + +``` +// 有如下常量资源 +BI.constant("bi.constant.items", [ + { + id: 1, + text: 1, + }, +]); +// 通过config方法拓展常量资源 +BI.config("bi.constant.items", items => { + items.push({ + id: 2, + text: 2, + }); + return items; +}); +``` + +4. 修改provider + +``` +// 定义provider +var Provider = function () { + + var items = [ + { + id: 1, + text: 1, + }, + ]; + + this.inject = function (item) { + items.push(item); + }; + + this.$get = function () { + return BI.inherit(BI.OB, { + getItems: function () { + return items.slice(0); + }, + }); + }; +}; + +BI.provider("bi.provider.demo", Provider); + +// 配置provider,对外开放的拓展接口大多采用provider的形式. 这样有什么好处? 和直接配置constant有什么比优点吗 +BI.config("bi.provider.demo", (provider) => { + provider.inject({ + id: 2, + text: 2, + }); +}); +``` + +5. 配置一些系统级的配置 + +``` +BI.config("bi.provider.system", (provider) => { + // Size相关,响应式,worker,模块依赖等 +}); +``` \ No newline at end of file diff --git a/images/34.png b/images/34.png new file mode 100644 index 0000000..992d87c Binary files /dev/null and b/images/34.png differ diff --git a/images/35.png b/images/35.png new file mode 100644 index 0000000..a95c55a Binary files /dev/null and b/images/35.png differ diff --git a/images/38.gif b/images/38.gif new file mode 100644 index 0000000..86a2a04 Binary files /dev/null and b/images/38.gif differ diff --git a/images/39.gif b/images/39.gif new file mode 100644 index 0000000..1311a7f Binary files /dev/null and b/images/39.gif differ diff --git a/questions/28.几个常用的布局场景.md b/questions/28.几个常用的布局场景.md index 0b1644b..64493c4 100644 --- a/questions/28.几个常用的布局场景.md +++ b/questions/28.几个常用的布局场景.md @@ -85,3 +85,61 @@ const widget = { ![示例5](../images/31.png) ![示例6](../images/32.png) + +多层级嵌套时,避免在顶层设置gap属性 +例如常用的上面导航下面tab示例,由于外层组件设置了`hgap:10`,因此即使内部`bi.vertical`组件出现滚动条,也无法贴附在容器边缘 +因此开发过程中药尽量将gap设置下沉到具体的子组件 + +```js +const widget = { + type: "bi.vtape", + cls: "bi.border", + width: 300, + hgap: 10, + items: [ + { + type: "demo.nav", + height: 36, + }, + { + type: "bi.tab", + showIndex: 1, + cardCreator: v => { + return { + type: "bi.vertical", // 假设这个布局会出现滚动条 + }; + }, + }, + ], +}; +``` + +进行一次额外的布局包装,多一层DOM,收获一份体验 +因为vertical2没有高度,因此其会被子节点撑起高度,而后导致vertical1发生溢出,出现滚动条 +这样既实现了滚动条贴边,又保证了每一个列表项的左右间距. + +```js +const widget3 = { + type: "bi.vertical", // vertical1 + cls: "bi-border vertical1", + hgap: 10, + height: 300, + width: 300, + items: [ + { + type: "bi.vertical", // vertical2,假设这个布局会出现滚动条, + cls: "vertical2", + items: (new Array(100)).fill(0).map((v, i) => { + return { + type: "bi.label", + cls: "bi-list-item-active2", + text: `第${i}项`, + }; + }), + }, + ], +}; +``` + +![示例7](../images/34.png) +![示例8](../images/35.png) diff --git a/questions/29.BI.Layers.create参数详解原理.md b/questions/29.BI.Layers.create参数详解原理.md new file mode 100644 index 0000000..18dd443 --- /dev/null +++ b/questions/29.BI.Layers.create参数详解原理.md @@ -0,0 +1,61 @@ +# BI.Layers.create方法参数原理解释 + +今天看了一个BUG,通过BI.Layers创建的蒙层,在窗口大小改变时候没有随之改变。 + +先看FineUI中Layer.create方法的描述 + +```typescript +create(name: string, from?: any, op?: any, context?: any): T; +``` + +推荐的统一写法 + +```javascript +const name; // your name +const container; // your container +BI.Layers.create(name, null, { + container: container, + render: { + type: "xxx", + }, +}).show(name); +``` + +我们重点关注`from`属性和`op.container`属性 + +from属性控制的是,layer相对定位的元素.通过控制 `position: fixed` 搭配top,left,width,height属性,遮罩覆盖到对于元素上.同时监听元素resize,动态的更新宽高,使之一直"覆盖"在目标元素上 + +```javascript +const name = BI.UUID(); +const form = this.element; +BI.Layers.create(name, form, { + type: "xxx", +}).show(name); +``` + +这样实现有什么弊端吗?举个例子,具体业务中经常有需要一个layer遮罩做一些具体操作的场景. 以定时调度新建任务为例:创建layer后,拖动容器宽度,会有明显的延迟. +这是因为resize事件的监听并不是实时的,做了节流.因此导致触发更新宽度慢了一拍. + +![示例](../images/38.gif) + +`op.container`属性又是什么作用呢,其控制的是弹出层在DOM树中属于哪个节点的子节点.默认情况下Layer弹出层是挂载到body下面的. +指定container之后,layer层将以position:absolute的形式,挂载到对于container节点下面. + +当未指定form参数的时候,Layer层将以绝对布局形式固定相对container定位.在此场景下,宽高尺寸的动态调整全由浏览器渲染引擎自动控制. +根据[absolute定位的特性](./41.绝对布局的隐藏知识点.md),当`top:0;left:0`且元素未指定宽度时,绝对定位元素将随定位元素动态改变大小 +这样再调整宽高,缩放等场景下表现就很顺滑了 + +```javascript +const name = BI.UUID(); +BI.Layers.create(name, null, { + container: this.element, + render: { + type: "xxx", + }, +}).show(name); +``` + +![示例2](../images/39.gif) + +既然指定form参数有这些弊端,为什么还要保留这个设计呢.当遇到layer弹出层中存在下拉框,气泡等特殊场景的时候.layer挂在到body上面可以避免这些痛点. +当然这种场景属于特事特办,需要开发者对stacking context和combo原理有深入的理解