TinyVue 的开源征程
实现数据的双向绑定 实现面向对象的 JS 库 配置式开发的注册表
封装成 Vue 组件 后端服务适配器 标签式与配置式
开发组件库面临的问题 面向逻辑编程与无渲染组件 跨端跨技术栈TODO组件示例
<html ng-app="myApp"><head><script src="angular.min.js">script>head><body><div ng-controller="myController"><input type="text" ng-model="message"><p>{{ message }}p>div><script>// 创建一个名为"myApp"的AngularJS模块var app = angular.module('myApp', []);// 在"myApp"模块下定义一个控制器"myController"app.controller('myController', function($scope) {$scope.message = "Hello AngularJS"; // 初始值// 监听message的变化$scope.$watch('message', function(newValue, oldValue) {console.log('新值:', newValue);console.log('旧值:', oldValue);});});script>body>html>
type="text" ng-model="message"><p>{{ message }}p>$scope.message = "Hello AngularJS";function createScope() {var me = this;var attrs = {};var watches = {};function Scope() {}Scope.prototype = {/*** 添加属性* @param {String} attrName 属性名* @param {Object} attrValue 属性值* @returns {Scope}*/$addAttribute: function(attrName, attrValue, readOnly) {if (attrName && typeof attrName === "string" && !attrs[attrName] && attrName.indexOf("$") !== 0) {Object.defineProperty(attrObject, attribute, {get: function() {return attrs[attrName].value;},set: function(newValue) {var attr = attrs[attrName];var oldValue = attr.value;var result = false;var watch;// 判断新旧值是否完全相等if (oldValue !== newValue && !(oldValue !== oldValue && newValue !== newValue)) {watch = watches[attrName];// 是否有监听该属性的回调函数if (watch && !attr.watching) {attr.watching = true;result = watch.callbacks.some(function(callback) {try {// 如果监听回调函数返回 false,则终止触发下一个回调函数return callback.call(scope, newValue, oldValue, scope) === false;} catch (e) {me.error(e);}});delete attr.watching;}if (!result) {if (attr.watching) {try {// 如果监听回调函数执行过程中又更改值,则抛出异常throw new Error("Cannot change the value of '" + attrName + "' while watching it.");} catch (e) {me.error(e, 2);}} else {// 所有监听回调函数执行完,再赋予新值attr.value = newValue;}}}},enumerable: true,configurable: true});}return this;},/*** 监听属性变化* @param {String} attrName 属性名* @param {Function} callback 监听回调函数* @param {Number} [priority] 监听的优先级* @returns {Scope}*/$watch: function(attrName, callback, priority) {if (attrName && typeof attrName === "string" && attrs[attrName] && typeof callback === "function") {var watch = watches[attrName] || {callbacks: [],priorities: [],minPriority: 0,maxPriority: 0};var callbacks = watch.callbacks;var priorities = watch.priorities;var nIndex = callbacks.length;if (typeof priority !== "number") {priority = me.CALLBACK_PRIORITY;}// 判断监听回调函数的优先级if (priority > watch.minPriority) {if (priority > watch.maxPriority) {// 优先级数值最高的监听回调函数放在队尾callbacks.push(callback);priorities.push(priority);watch.maxPriority = priority;} else {priorities.some(function(item, index) {if (item > priority) {nIndex = index;return true;}});// 按照优先级的数值在队列适当位置插入监听回调函数callbacks.splice(nIndex, 0, callback);priorities.splice(nIndex, 0, priority);}} else {// 优先级数值最小的监听回调函数放在队首callbacks.unshift(callback);priorities.unshift(priority);watch.minPriority = priority;}watches[attrName] = watch;}return this;}}// 返回 Scope 类新建的实例,即 $scope 变量return new Scope();}
// 定义 A 类jClass.define('A', {privates: {name1: '1' // 私有成员,内部可以访问,但子类不能访问},protects: {name2: '2' // 保护成员,子类可以访问,类的实例不能访问},publics: {name3: '3' // 公有成员,子类可以访问,类的实例可以访问}});// 定义 B 类,继承 A 类jClass.define('B', {extend: ['A'],publics: {// 公有方法,子类可以访问,类的实例可以访问say: function(str) {alert(str + this.name2); // 可以访问父类的保护成员 name2,但无法访问其私有成员 name1}}});// 创建 B 类的实例var b = jClass.create('B');b.say(b.name3); // B 继承 A 的 name3 属性,由于类的实例可以访问公有成员,因此弹出框内容为 3
// 定义 C 类jClass.define('C', {// 类的构造函数,初始化 title 属性init: function(title) {this.title = title; // 设置保护成员 title 的值},protects: {title: '' // 保护成员,子类可以访问,类的实例不能访问}});// 定义 D 类,同时继承 B 类和 C 类jClass.define('D', {extend: ['B', 'C'],publics: {// 公有方法,类的实例可以访问say: function() {alert(this.title + this.name2); // 访问 C 的保护成员 title 和 B 的保护成员 name2}}});// 创建 D 的实例,传入 C 构造函数所需的参数,弹出框内容为 32jClass.create('D', ['3']).say();
// 创建一个工厂的实例var tg = jClass.factory('triggerFactory', {// 类的构造函数,初始化 name 属性init: function(name) {this.name = name;},// 公有成员,类的实例可以访问publics: {name: '',show: function(str) {this.name += 'x' + str;return this;},hide: function(str) {this.$fire('gone', [str]); // 抛出名为 gone 的事件,事件参数为 str}}});// 通过工厂定义一个名为 tg1 的类tg.define('tg1', {publics: {name: '1' // 重载工厂的 name 属性值}});var resShow = '', resName = '', resGone = '';// 监听 tg1 类的 show 方法调用,如果该方法被执行,则触发以下函数tg.on('show', 'tg1', function(str) {resShow = this.name + '=' + str;});// 监听 tg1 类的 name 属性设置,如果该属性被重新赋值,则触发以下函数tg.on('name', 'tg1', function(oldValue, newValue) {resName = newValue;});// 监听所有子类的 gone 事件,如果该事件被抛出,则触发以下函数tg.on('gone', function(str) {resGone = str;});// 创建 tg1 的实例,执行该实例的 show 方法和 hide 方法tg.create('tg1').show('2').hide('3');alert(resShow + ' ' + resName + ' ' + resGone); // 弹出框内容为 1x2=2 1x2 3
// 定义组件的基类 WidgetjClass.define('Widget', {// 组件的构造函数,设置宽度和标题init: function(width, title) {this.width = width;this.title = title;this.setup(); // 初始化组件的属性this.compile(); // 根据组件属性编译组件模板this.render(); // 渲染输出组件的 HTML 字符串},protects: {width: 0,title: '',templet: '',// 初始化组件的属性setup: function() {this.width = this.width + 'px';},// 根据组件属性编译组件模板compile: function() {this.templet = this.templet.replace(/{{:width}}/g, this.width);},// 渲染输出组件的 HTML 字符串render: function() {this.html = this.templet.replace(/{{:title}}/g, this.title);}},publics: {html: '' // 记录组件的 HTML 字符串}});// 定义一个 Button 组件,继承 Widget 基类jClass.define('Button', {extend: ['Widget'],protects: {// 设置 Button 组件的模板templet: ''}});// 创建一个 Button 组件的实例jClass.create('Button', [100, 'OK']).html; // 返回// 定义一个 LongButton 组件,继承 Button 父类jClass.define('LongButton', {extend: ['Button'],protects: {// 重载父类的 setup 方法,将长度自动加 100setup: function() {this.width = this.width + 100 + 'px';}}});// 创建一个 LongButton 组件的实例jClass.create('LongButton', [100, 'OK']).html; // 返回
系统生命周期:系统生命周期指的是整个前端应用的生命周期,它包含了应用的启动、初始化、运行和关闭等阶段。系统生命周期提供了应用级别的钩子函数,例如应用初始化前后的钩子、应用销毁前后的钩子等。通过系统生命周期的支持,开发者可以在应用级别执行一些全局的操作,例如加载配置、注册插件、处理全局错误等。 页面生命周期:页面生命周期指的是单个页面的生命周期,它描述了页面从加载到卸载的整个过程。页面生命周期包含了页面的创建、渲染、更新和销毁等阶段。在页面生命周期中,HAE 前端框架提供了一系列钩子函数,例如页面加载前后的钩子、页面渲染前后的钩子、页面更新前后的钩子等。通过页面生命周期的支持,开发者可以在页面级别执行一些与页面相关的逻辑,例如获取数据、处理路由、初始化页面状态等。 组件生命周期:组件生命周期指的是单个组件的生命周期,它描述了组件从创建到销毁的整个过程。组件生命周期包含了组件的实例化、挂载到 DOM、更新和卸载等阶段。组件生命周期的钩子函数与页面生命周期类似,通过组件生命周期的支持,开发者可以在组件级别执行一些与组件相关的逻辑,例如初始化状态、处理用户交互、与外部组件通信等。
framework: {load_modules: { // 加载框架所需的模块hae_runtime: true,extend_modules: true},set_config: { // 初始化框架的服务配置Hae_Service_Mock: true,Hae_Service_Environment: true,Hae_Service_Ajax: true,Hae_Service_Personalized: true,Hae_Service_Permission: true,Hae_Service_DataSource: true},init_runtime: { // 框架运行过程中所需的服务Hae_Service_Router: true,Hae_Service_Message: true,Hae_Service_Popup: true,},boot_system: { // 启动框架的模板引擎和页面渲染Hae_Service_Templet: true,Hae_Service_Page: true},start_services: { // 运行用于处理全局错误和日志信息的服务Hae_Service_DebugToolbar: true,Hae_Service_LogPanel: true}}
framework: {load_modules: { // 加载框架所需的模块},set_config: { // 初始化框架的服务配置Hae_Service_Mock: false,Hae_Service_Environment: true,Hae_Service_Ajax: true,Hae_Service_BehaviorAnalysis: trueHae_Service_Personalized: true,Hae_Service_Permission: true,Hae_Service_DataSource: true,},init_runtime: { // 框架运行过程中所需的服务},boot_system: { // 启动框架的模板引擎和页面渲染},start_services: { // 运行用于处理全局错误和日志信息的服务}}
/*** 请求模拟服务* @class Hae.Service.Mock*/Hae.loadService({id: 'Hae.Service.Mock', // 服务 IDname: 'HAE_SERVICE_MOCK', // 服务名称callback: function() {} // 服务的钩子函数});
/*** 请求模拟服务* @class Hae.Service.Mock*/Hae.define('Hae.Service.Mock', { // 服务 IDservices: {// 服务名称HAE_SERVICE_MOCK: function() {} // 服务的钩子函数}}
page: {loadHtml: {Hae_Service_Page: "loadHtml"},initContext: {Hae_Service_Page: "initContext"},loadJs: {Hae_Service_Page: "loadJs"},loadContent: {Hae_Service_Locale: "translate",Hae_Service_Page: "loadContent",Hae_Service_Personalized: "setRoutePath"},compile: {Hae_Service_Permission: "compile",Hae_Service_DataBind: "compile",Hae_Service_ActionController: "compile",Hae_Service_ValidationController: "compile"},render: {Hae_Service_Page: "render"},complete: {Hae_Service_DataBind: "complete",Hae_Service_ActionController: "complete",Hae_Service_ValidationController: "complete"},domReady: {Hae_Service_Page: "domReady",Hae_Service_DataBind: "domReady",Hae_Service_TipHelper: "initHelper"}}
/*** 数据双向绑定* @class Hae.Service.DataBind*/Hae.define('Hae.Service.DataBind', {services: {HAE_SERVICE_DATABIND_COMPILE: function(pageScope) {},HAE_SERVICE_DATABIND_COMPLETE: function(pageScope) {},HAE_SERVICE_DATABIND_DOMREADY: function(pageScope) {}}}
/*** 所有 Widget 组件的基类* @class Hae.Widget* @extends Hae*/Hae.define('Hae.Widget', {protects: {/*** 初始化阶段*/setup: function() {},/*** 编译模板阶段,返回异步对象* @returns {Hae.Promise}*/compile: function() {this.template = Hae.create('Hae.Compile').compile(this.widgetType, this.op);return Hae.deferred().resolve();},/*** 渲染模板阶段,返回异步对象* @returns {Hae.Promise}*/render: function() {this.dom.html(Hae.create('Hae.Render').render(this.template, this.dataset, this.op));},/*** 绑定事件阶段*/bind: function() {},/*** 组件完成阶段*/complete: function() {},/*** 组件销毁*/destroy: function() {}}}
声明式编程:Vue 采用了声明式编程的思想,开发者可以通过声明式的模板语法编写组件的结构和行为,而无需直接操作 DOM。这简化了开发流程并提高了开发效率。 组件化开发:Vue 鼓励组件化开发,将 UI 拆分为独立的组件,每个组件具有自己的状态和行为。这样可以实现组件的复用性、可维护性和扩展性,提高了代码的可读性和可维护性。 响应式数据绑定:Vue 采用了响应式数据绑定的机制,将数据与视图自动保持同步。当数据发生变化时,自动更新相关的视图部分,大大简化了状态管理的复杂性。 自动化流程:前端工程化引入了自动化工具,例如构建工具(例如 Webpack)、任务运行器(例如 npm)和自动化测试工具,大大简化了开发过程中的重复性任务和手动操作。通过自动化流程,开发者可以自动编译、打包、压缩和优化代码,自动执行测试和部署等,提高了开发效率和一致性。 模块化开发:前端工程化鼓励使用模块化开发的方式,将代码拆分为独立的模块,每个模块负责特定的功能。这样可以提高代码的可维护性和复用性,减少了代码之间的耦合性,使团队协作更加高效。 规范化与标准化:前端工程化倡导遵循一系列的规范和标准,包括代码风格、目录结构、命名约定等。这样可以提高团队协作的一致性,减少沟通和集成的成本,提高项目的可读性和可维护性。 静态类型检查和测试:前端工程化鼓励使用静态类型检查工具(例如 TypeScript)和自动化测试工具(例如 Mocha)来提高代码质量和稳定性。通过静态类型检查和自动化测试,可以提前捕获潜在的错误和问题,减少Bug的产生和排查的时间。
app.component("my-slider", {props: ["modelValue"],// always an empty divtemplate: "",watch: {// updates component when the bound value changesvalue: {handler(value) {webix.$$(this.webixId).setValue(value);},},},mounted() {// initializes Webix Sliderthis.webixId = webix.ui({// container and scope are mandatory, other properties are optionalcontainer: this.$el,$scope: this,view: "slider",value: this.modelValue,});// informs Vue about the changed value in case of 2-way data binding$$(this.webixId).attachEvent("onChange", function() {var value = this.getValue();// you can use a custom event herethis.$scope.$emit("update:modelValue", value);});},// memory cleaningdestroyed() {webix.$$(this.webixId).destructor();},});
export default {name: 'AuiSlider',render: function (createElement) {// 渲染一个 div 标签,类似于 Webix 的 template: ""return createElement('div', this.$slots.default)},props: ['modelValue', 'op'],data() {return {widget: {}}},created() {// 在 Vue 组件的创建阶段监听 value 属性this.$watch('value', (value) => {// 一旦它发生变化则调用 widget 的 setValue 方法重新 HAE 组件的值this.widget.setValue && this.widget.setValue(value)})},methods: {createcomp() {let dom = $(this.$el)let fullOp = this.$props['op']let extendOp = {// 监听 HAE 组件的 `onChange` 事件onChange: (val) => {// 抛出 Vue 的 `update:modelValue` 事件this.$emit('update:modelValue', val)}}// 获取 Vue 组件的配置参数let op = fullOp? Hae.extend({}, fullOp, extendOp, { value: this.$props.value }): Hae.extend({}, this.$props, extendOp)this.$el.setAttribute('widget', 'Slider')// 调用 Hae 的 widget 方法动态创建了一个 HAE 组件this.widget = Hae.widget(dom, 'Slider', op)}},mounted() {// 在 Vue 组件的挂载阶段创建 HAE 组件this.createcomp()}}
解耦前后端:适配器充当前后端之间的中间层,将前端组件与后端服务解耦。通过适配器,前端组件不需要直接了解或依赖于后端服务的具体接口和数据格式。这种解耦使得前端和后端能够独立地进行开发和演进,而不会相互影响。 统一接口:不同的后端服务可能具有不同的接口和数据格式,这给前端组件的开发带来了困难。适配器的作用是将不同后端服务的接口和数据格式转化为统一的接口和数据格式,使得前端组件可以一致地与适配器进行交互,而不需要关心底层后端服务的差异。 灵活性和扩展性:通过适配器,前端组件可以轻松地切换和扩展后端服务。如果需要替换后端服务或新增其他后端服务,只需添加或修改适配器,而不需要修改前端组件的代码。这种灵活性和扩展性使得系统能够适应不同的后端服务需求和变化。 隐藏复杂性:适配器可以处理后端服务的复杂性和特殊情况,将这些复杂性隐藏在适配器内部。前端组件只需与适配器进行交互,无需关注后端服务的复杂逻辑和细节。这种抽象和封装使得前端组件的开发更加简洁和高效。
Setting.services = {Area: 'servlet/idataProxy/params/ws/soaservices/AreaServlet',Company: 'servlet/idataProxy/params/ws/soaservices/CompanyServlet',Country: 'servlet/idataProxy/params/ws/soaservices/CountryServlet',Currency: 'servlet/idataProxy/params/ws/soaservices/CurrencyServlet',}
Setting.services = {Area: 'services/saasIdatasaasGetGeoArea',Company: 'services/saasIdatasaasGetCompany',Country: 'services/saasIdatasaasGetCountry',Currency: 'services/saasIdatasaasGetCurrency',}
class Aurora {// 注册后端服务适配器get registerService() {}// 返回后端服务适配器的实例get getServiceInstance() {}// 删除后端服务适配器的实例get destroyServiceInstance() {}// 基础服务,主要提供获取和设置环境信息、用户信息、菜单、语言、权限等数据信息的方法get base() {return getService().base}// 通用服务,主要提供和业务(地区、部门等)相关的方法get common() {return getService().common}// 消息服务,主要用于订阅消息、发布消息、取消订阅get message() {return getService().message}// 网络服务,基于 axios 实现的,用法和 axios 基本相同,只支持异步请求get network() {return getService().network}// 存储服务,默认基于 window.localstorage 方法扩展get storage() {return getService().storage}// 权限服务,校验当前用户是否有该权限点的某个操作权限。计算权限点,支持标准逻辑运算符 |, &get privilege() {return getService().privilege}// 资源服务,主要是资源请求,追加配置资源路径,用于加载依赖库,从公共库,组件目录,或者远程加载get resource() {return resource}}
class JalorService {constructor(config = {}) {this.utils = utils(this)// 注册服务到全局的适配器实例中Aurora.registerService(this, config)this.ajax = ajax(this)this.init = init(this)// 初始化适配器的配置信息config.services = services(this)config.options = options(this)config.widgets = widgets(this)// 需要使用柯里化函数初始化的服务方法this.fetchEnvService = fetchEnvService(this)this.fetchLangResource = fetchLangResource(this)this.fetchArea = fetchArea(this)this.fetchCompany = fetchCompany(this)this.fetchCountry = fetchCountry(this)this.fetchCurrency = fetchCurrency(this)this.fetchFragment = fetchFragment(this)// 其他需要初始化的服务方法fetchDeptList(this)fetchUser(this)fetchLocale(this)fetchLogout(this)fetchRole(this)fetchCustomized(this)fetchEdoc(this)}// 服务适配器的名称get name() {return 'jalor'}}export default JalorService
export default function (instance) {return ({ label, parent }) => {return new Promise((resolve, reject) => {// 调用 @aurora/core 的 network 网络服务发送请求instance.network.get(instance.setting.services.Area, {params: {'area_label': label,'parent': parent}}).then((response) => {resolve(response.data.area)}).catch(reject)})}}
export default function (instance) {return ({ label, parent }) => {return new Promise((resolve, reject) => {// 调用 @aurora/core 的 network 网络服务发送请求instance.network.get(instance.setting.services.Area, {params: {'geo_org_type': label,'parent': parent}}).then(response => {resolve(response.data.BO)}).catch(reject)})}}
this.$service.common.getArea({ label: 'Region', parent: '1072' }).then(data => { console.log(data) })var op = {min: 0,max: 100,step: 10,range: 'min'}// 调用 Hae 的 widget 方法动态创建了一个 HAE 组件this.widget = Hae.widget(dom, 'Slider', op)
<template><aui-slider v-model="value" :min="0" :max="100" :step="10" :range="min">aui-slider>template><script>import { Slider } from '@aurora/vue'export default {components: {AuiSlider: Slider},data() {return {value: 30}}}script>
<template><aui-slider v-model="value" :op="op">aui-slider>template><script>import { Slider } from '@aurora/vue'export default {components: {AuiSlider: Slider},data() {return {value: 30,op: {min: 0,max: 100,step: 10,range: 'min'}}}}script>
简化 DSL 开发流程:配置式声明将组件的配置信息集中在一个对象中,低代码 DSL 开发人员可以通过修改对象的属性值来自定义组件的行为和外观。这种方式避免生成繁琐的标签嵌套和属性设置,简化了 DSL 的开发流程。 提高配置的可复用性:配置式声明可以将组件的配置信息抽象为一个可重复使用的对象,可以在多个组件实例中共享和复用。低代码平台开发人员可以定义一个通用的配置对象,然后在不同的场景中根据需要进行定制,减少了重复的代码编写和配置调整。 动态生成配置信息:配置式声明允许低代码平台开发人员使用变量、动态表达式和逻辑控制来低代码组件配置面板生成的配置信息。这样可以根据不同的条件和数据来动态调整组件的配置,增强了组件配置面板的灵活性和适应性。 可视化配置界面:配置式声明通常与可视化配置界面相结合,低代码平台的使用人员可以通过低代码的可视化界面直接修改物料组件的属性值。这种方式使得配置更直观、易于理解,提高了开发效率。 适应复杂业务场景:在复杂的业务场景中,组件的配置信息可能会非常繁琐和复杂。通过配置式声明,低代码物料组件的开发人员可以更方便地管理和维护大量的配置属性,减少了出错的可能性。
追踪当前文件夹的状态,展示其内容 处理文件夹的相关操作 (打开、关闭和刷新) 支持创建新文件夹 可以切换到只展示收藏的文件夹 可以开启对隐藏文件夹的展示 处理当前工作目录中的变更
逻辑与 UI 分离:将逻辑和 UI 分离,使得代码更易于理解和维护。通过将逻辑处理和数据转换等任务抽象成无渲染组件,可以将关注点分离,提高代码的可读性和可维护性。 提高可重用性:组件的逻辑可以在多个场景中重用。这些组件不依赖于特定的 UI 组件或前端框架,可以独立于界面进行测试和使用,从而提高代码的可重用性和可测试性。 符合单一职责原则:这种设计鼓励遵循单一职责原则,每个组件只负责特定的逻辑或数据处理任务。这样的设计使得代码更加模块化、可扩展和可维护,减少了组件之间的耦合度。 更好的可测试性:由于无渲染组件独立于 UI 进行测试,可以更容易地编写单元测试和集成测试。测试可以专注于组件的逻辑和数据转换,而无需关注界面的渲染和交互细节,提高了测试的效率和可靠性。 提高开发效率:开发人员可以更加专注于业务逻辑和数据处理,而无需关心具体的 UI 渲染细节。这样可以提高开发效率,减少重复的代码编写,同时也为团队协作提供了更好的可能性。
添加待办事项:在输入框输入待办事项信息,点击右边的 Add 按钮后,下面待办事项列表将新增一项刚输入的事项信息。 删除待办事项:在待办事项列表里,选择其中一个事项,点击右边的X按钮后,该待办事项将从列表里清除。 移动端展示:当屏幕宽度缩小时,组件将自动切换成如下 Mobile 的展示形式,功能仍然保持不变,即输入内容直接按回车键添加事项,点击 X 删除事项。
// 普通函数var add = function(x, y) {return x + y}add(3, 4) // 返回 7// 柯里化函数var foo = function(x) {return function(y) {return x + y}}foo(3)(4) // 返回 7
/*** 添加一个标签,给定一个 tag 内容,往已有标签集合里添加该 tag** @param {object} text - 输入框控件绑定数据* @param {object} props - 组件属性对象* @param {object} refs - 引用元素的集合* @param {function} emit - 抛出事件的方法* @param {object} api - 暴露的API对象* @returns {boolean} 标签是否添加成功*/const addTag = ({ text, props, refs, emit, api }) => tag => {// 判断 tag 内容是否为字符串,如果不是则取输入框控件绑定数据的值tag = trim(typeof tag === 'string' ? tag : text.value)// 检查已存在的标签集合里是否包含新 tag 的内容if (api.checkTag({ tags: props.tags, tag })) {// 如果已存在则返回添加失败return false}// 从组件属性对象获取标签集合,往集合里添加新 tag 元素props.tags.push(tag)// 清空输入框控件绑定数据的值text.value = ''// 从引用元素集合里找到输入控件,让其获得焦点refs.input.focus()// 向外抛出事件,告知已添加新标签emit('add', tag)// 返回标签添加成功return true}/*** 移除一个标签,给定一个 tag 内容,从已有标签集合里移除该 tag** @param {object} props - 组件属性对象* @param {object} refs - 引用元素的集合* @param {function} emit - 抛出事件的方法* @returns {boolean} 标签是否添加成功*/const removeTag = ({ props, refs, emit }) => tag => {// 从组件属性对象获取标签集合,在集合里查找 tag 元素的位置const index = props.tags.indexOf(tag)// 如果位置不是-1,则表示能在集合里找到对应的位置if (index !== -1) {// 从组件属性对象获取标签集合,在集合的相应位置移除该 tag 元素props.tags.splice(index, 1)// 从引用元素集合里找到输入控件,让其获得焦点refs.input.focus()// 向外抛出事件,告知已删除标签emit('remove', tag)// 返回标签移除成功return true}// 如果找不到则返回删除失败return false}// 向上层暴露业务逻辑方法export {addTag,removeTag}
// Vue适配层,负责承上启下,即引入下层的业务逻辑方法,自动构造标准的适配函数,提供给上层的模板视图使用import { addTag, removeTag, checkTag, focus, inputEvents, mounted } from 'business.js'/*** 无渲染适配函数,根据 Vue 框架的差异性,为业务逻辑方法提供所需的原材料** @param {object} props - 组件属性对象* @param {object} context - 页面上下文对象* @param {function} value - 构造双向绑定数据的方法* @param {function} onMounted - 组件挂载时的方法* @param {function} onUpdated - 数据更新时的方法* @returns {object} 返回提供给上层模板视图使用的 API*/export const renderless = (props, context, { value, onMounted, onUpdated }) => {// 通过页面上下文对象获取父节点元素const parent = context.parent// 通过父节点元素获取输入框控件绑定数据const text = parent.text// 通过父节点元素获取其上下文对象,再拿到抛出事件的方法const emit = parent.$context.emit// 通过页面上下文对象获取引用元素的集合const refs = context.refs// 以上为业务逻辑方法提供所需的原材料,基本是固定的,不同框架有所区别// 初始化输入框控件绑定数据,如果没有定义则设置为空字符串parent.text = parent.text || value('')// 构造返回给上层模板视图使用的 API 对象const api = {text,checkTag,focus: focus(refs),// 第一次执行 removeTag({ props, refs, emit }) 返回一个函数,该函数用来给模板视图的 click 事件removeTag: removeTag({ props, refs, emit })}// 在组件挂载和数据更新时需要处理的方法onMounted(mounted(api))onUpdated(mounted(api))// 与前面定义的 API 对象内容进行合并,新增 addTag 和 inputEvents 方法return Object.assign(api, {// 第一次执行 addTag({ text, props, refs, emit, api }) 返回一个函数,该函数用来给模板视图的 click 事件addTag: addTag({ text, props, refs, emit, api }),inputEvents: inputEvents({ text, api })})}
import { addTag, removeTag, checkTag, focus, inputEvents, mounted } from 'business.js'export const renderless = (props, context, { value, onMounted, onUpdated }) => {const text = value('')const emit = context.emitconst refs = context.refsconst api = {text,checkTag,focus: focus(refs),removeTag: removeTag({ props, refs, emit })}onMounted(mounted(api))onUpdated(mounted(api), [context.$mode])return Object.assign(api, {addTag: addTag({ text, props, refs, emit, api }),inputEvents: inputEvents({ text, api })})}
import { renderless, api } from '../../renderless/Todo/vue'import { props, setup } from '../common'export default {props: [...props, 'newTag', 'tags'],components: {TodoTag: () => import('../Tag')},setup(props, context) {return setup({ props, context, renderless, api })}}
import { useRef } from 'react'import { renderless, api } from '../../renderless/Todo/react'import { setup, render, useRefMapToVueRef } from '../common/index'import pc from './pc'import mobile from './mobile'import '../../theme/Todo/index.css'export default props => {const { $mode = 'pc', $template, $renderless, listeners = {}, tags } = propsconst context = {$mode,$template,$renderless,listeners}const ref = useRef()useRefMapToVueRef({ context, name: 'input', ref })const { addTag, removeTag, inputEvents: { keydown, input }, text: { value } } = setup({ context, props, renderless, api, listeners, $renderless })return render({ $mode, $template, pc, mobile })({ addTag, removeTag, value, keydown, input, tags, ref, $mode })}
<template><div align="center"><slot name="header">slot><div align="left" class="max-w-md w-full mx-auto"><div class="form-group d-flex"><input ref="input" :value="text" :placeholder="newTag" v-on="inputEvents" class="aui-todo aui-font border border-primary shadow-none rounded-0 d-inline todo-input"><button class="btn btn-primary shadow-none border-0 rounded-0" @click="addTag">Addbutton>div><div class="list-group"><div class="list-group-item d-flex justify-content-between align-items-center" v-for="tag in tags" :key="tag"><todo-tag :$mode="$mode" :content="tag" /><button class="close shadow-none border-0" @click="removeTag(tag)"><span>×span>button>div>div>div><slot name="footer">slot>div>template>
Add
<template><div class="todo-mobile" align="center"><slot name="header">slot><div align="left" class="max-w-md w-full mx-auto"><div class="tags-input"><span class="tags-input-tag" v-for="tag in tags" :key="tag"><todo-tag :$mode="$mode" :content="tag" /><button type="button" class="tags-input-remove" @click="removeTag(tag)">×button>span><input ref="input" :value="text" :placeholder="newTag" v-on="inputEvents" class="aui-todo aui-font tags-input-text">div>div><slot name="footer">slot>div>template>
import React from 'react'import Tag from '../Tag'export default props => {const { addTag, removeTag, value, keydown, input, tags, ref, $mode } = propsreturn (<div align="left" className="max-w-md w-full mx-auto"><div className="form-group d-flex"><input ref={ref} value={value} onChange={input} onKeyDown={keydown} placeholder="New Tag" type="text" className="aui-todo aui-font border border-primary shadow-none rounded-0 d-inline todo-input" /><button className="btn btn-primary shadow-none border-0 rounded-0" onClick={addTag}>Addbutton>div><div className="list-group">{tags.map(tag => {return (<div key={tag} className="list-group-item d-flex justify-content-between align-items-center"><Tag content={tag} $mode={$mode} /><button className="close shadow-none border-0" onClick={() => { removeTag(tag) }}><span>×span>button>div>)})}div>div >)}
import React from 'react'import Tag from '../Tag'import '../../style/mobile.scss'export default props => {const { removeTag, value, keydown, input, tags, ref, $mode } = propsreturn (<div className="todo-mobile" align="center"><div align="left" className="max-w-md w-full mx-auto"><div className="tags-input">{tags.map(tag => {return (<span key={tag} className="tags-input-tag" ><Tag content={tag} $mode={$mode} /><button type="button" className="tags-input-remove" onClick={() => { removeTag(tag) }}>×button>span >)})}<input ref={ref} value={value} onChange={input} onKeyDown={keydown} placeholder="New Tag" className="aui-todo aui-font tags-input-text" />div>div>div>)}
按无渲染组件的设计模式,首先要将组件的逻辑分离成与技术栈无关的柯里化函数。 在定义组件的时候,借助面向逻辑编程的 API,比如 React 框架的 Hooks API、Vue 框架的 Composition API,将组件外观与组件逻辑完全解耦。 按不同终端编写对应的组件模板,再利用前端框架提供的动态组件,实现动态切换不同组件模板,从而满足不同外观的展示需求。