首页 文章 精选 留言 我的

精选列表

搜索[JDK 25],共10000篇文章
优秀的个人博客,低调大师

一道价值25k的腾讯递归组件面试题(Vue3 + TS 实现)

前言 小伙伴们好久不见,最近刚入职新公司,需求排的很满,平常是实在没时间写文章了,更新频率会变得比较慢。 周末在家闲着无聊,突然小弟过来紧急求助,说是面试腾讯的时候,对方给了个 Vue 的递归菜单要求实现,回来找我复盘。 正好这周是小周,没想着出去玩,就在家写写代码吧,我看了一下需求,确实是比较复杂,需要利用好递归组件,正好趁着这个机会总结一篇 Vue3 + TS 实现递归组件的文章。 需求 可以先在 Github Pages[1] 中预览一下效果。 需求是这样的,后端会返回一串可能有无限层级的菜单,格式如下: [{id:1,father_id:0,status:1,name:'生命科学竞赛',_child:[{id:2,father_id:1,status:1,name:'野外实习类',_child:[{id:3,father_id:2,status:1,name:'植物学'}],},{id:7,father_id:1,status:1,name:'科学研究类',_child:[{id:8,father_id:7,status:1,name:'植物学与植物生理学'},{id:9,father_id:7,status:1,name:'动物学与动物生理学'},{id:10,father_id:7,status:1,name:'微生物学'},{id:11,father_id:7,status:1,name:'生态学'},],},{id:71,father_id:1,status:1,name:'添加'},],},{id:56,father_id:0,status:1,name:'考研相关',_child:[{id:57,father_id:56,status:1,name:'政治'},{id:58,father_id:56,status:1,name:'外国语'},],},] 每一层的菜单元素如果有 _child 属性, 这一项菜单被选中以后就要继续展示这一项的所有子菜单,预览一下动图: 并且点击其中的任意一个层级,都需要把菜单的 完整的 id 链路 传递到最外层,给父组件请求数据用。比如点击了 科学研究类。那么向外 emit 的时候还需要带上它的第一个子菜单 植物学与植物生理学 的 id,以及它的父级菜单 生命科学竞赛 的 id,也就是 [1, 7, 8]。 每一层的样式还可以自己定制。 实现 这很显然是一个递归组件的需求,在设计递归组件的时候,我们要先想清楚数据到视图的映射。 在后端返回的数据中,数组的每一层可以分别对应一个菜单项,那么数组的层则就对应视图中的一行,当前这层的菜单中,被点击选中 的那一项菜单的 child 就会被作为子菜单数据,交给递归的 NestMenu 组件,直到某一层的高亮菜单不再有 child,则递归终止。 由于需求要求每一层的样式可能是不同的,所以再每次调用递归组件的时候,我们都需要从父组件的 props 中拿到一个 depth 代表层级,并且把这个 depth + 1 继续传递给递归的 NestMenu 组件。 重点主要就是这些,接下来编码实现。 先看 NestMenu 组件的 template 部分的大致结构: <template><divclass="wrap"><divclass="menu-wrap"><divclass="menu-item"v-for="menuItemindata">{{menuItem.name}}</div></div><nest-menu:key="activeId":data="subMenu":depth="depth+1"></nest-menu></div></template> 和我们预想设计中的一样, menu-wrap 代表当前菜单层, nest-menu 则就是组件本身,它负责递归的渲染子组件。 首次渲染 在第一次获取到整个菜单的数据的时候,我们需要先把每层菜单的选中项默认设置为第一个子菜单,由于它很可能是异步获取的,所以我们最好是 watch 这个数据来做这个操作。 //菜单数据源发生变化的时候默认选中当前层级的第一项constactiveId=ref<number|null>(null)watch(()=>props.data,(newData)=>{if(!activeId.value){if(newData&&newData.length){activeId.value=newData[0].id}}},{immediate:true,}) 现在我们从最上层开始讲起,第一层的 activeId 被设置成了 生命科学竞赛 的 id,注意我们传递给递归子组件的 data ,也就是 生命科学竞赛 的 child,是通过 subMenu 获取到的,它是一个计算属性: constgetActiveSubMenu=()=>{returndata.find(({id})=>id===activeId.value)._child}constsubMenu=computed(getActiveSubMenu) 这样,就拿到了 生命科学竞赛 的 child,作为子组件的数据传递下去了。 点击菜单项 回到之前的需求设计,在点击了菜单项后,无论点击的是哪层,都需要把完整的 id 链路通过 emit 传递到最外层去,所以这里我们需要多做一些处理: /***递归收集子菜单第一项的id*/constgetSubIds=(child)=>{constsubIds=[]consttraverse=(data)=>{if(data&&data.length){constfirst=data[0]subIds.push(first.id)traverse(first._child)}}traverse(child)returnsubIds}constonMenuItemClick=(menuItem)=>{constnewActiveId=menuItem.idif(newActiveId!==activeId.value){activeId.value=newActiveIdconstchild=getActiveSubMenu()constsubIds=getSubIds(child)//把子菜单的默认第一项ids也拼接起来向父组件emitcontext.emit('change',[newActiveId,...subIds])}} 由于我们之前定的规则是,点击了新的菜单以后默认选中子菜单的第一项,所以这里我们也递归去找子菜单数据里的第一项,放到 subIds 中,直到最底层。 注意这里的 context.emit("change", [newId, ...subIds]);,这里是把事件向上 emit,如果这个菜单是中间层级的菜单,那么它的父组件也是 NestMenu,我们需要在父层级递归调用 NestMenu 组件的时候监听这个 change 事件。 <nest-menu:key="activeId"v-if="activeId!==null":data="getActiveSubMenu()":depth="depth+1"@change="onSubActiveIdChange"></nest-menu> 在父层级的菜单接受到了子层级的菜单的 change 事件后,需要怎么做呢?没错,需要进一步的再向上传递: constonSubActiveIdChange=(ids)=>{context.emit('change',[activeId.value].concat(ids))} 这里就只需要简单的把自己当前的 activeId 拼接到数组的最前面,再继续向上传递即可。 这样,任意一层的组件点击了菜单后,都会先用自己的 activeId 拼接好所有子层级的默认 activeId,再一层层向上 emit。并且向上的每一层父菜单都会把自己的 activeId 拼在前面,就像接力一样。 最后,我们在应用层级的组件里,就可以轻松的拿到完整的 id 链路: <template><nest-menu:data="menu"@change="activeIdsChange"/></template>exportdefault{methods:{activeIdsChange(ids){this.ids=ids;console.log("当前选中的id路径",ids);},}, 样式区分 由于我们每次调用递归组件的时候,都会把 depth + 1,那么就可以通过把这个数字拼接到类名后面来实现样式区分了。 <template><divclass="wrap"><divclass="menu-wrap":class="`menu-wrap-${depth}`"><divclass="menu-item">{{menuItem.name}}</div></div><nest-menu/></div></template><style>.menu-wrap-0{background:#ffccc7;}.menu-wrap-1{background:#fff7e6;}.menu-wrap-2{background:#fcffe6;}</style> 默认高亮 上面的代码写完后,应对没有默认值时的需求已经足够了,这时候面试官说,产品要求这个组件能通过传入任意一个层级的 id 来默认展示高亮。 其实这也难不倒我们,稍微改造一下代码,在父组件里假设我们通过 url 参数或者任意方式拿到了一个 activeId,先通过深度优先遍历的方式查找到这个 id 的所有父级。 constactiveId=7constfindPath=(menus,targetId)=>{letidsconsttraverse=(subMenus,prev)=>{if(ids){return}if(!subMenus){return}subMenus.forEach((subMenu)=>{if(subMenu.id===activeId){ids=[...prev,activeId]return}traverse(subMenu._child,[...prev,subMenu.id])})}traverse(menus,[])returnids}constids=findPath(data,activeId) 这里我选择在递归的时候带上上一层的 id,在找到了目标 id 以后就能轻松的拼接处完整的父子 id 数组。 然后我们把构造好的 ids 作为 activeIds 传递给 NestMenu,此时这时候 NestMenu 就要改变一下设计,成为一个「受控组件」,它的渲染状态是受我们外层传递的数据控制的。 所以我们需要在初始化参数的时候改变一下取值逻辑,优先取 activeIds[depth] ,并且在点击菜单项的时候,要在最外层的页面组件中,接收到 change 事件时,把 activeIds 的数据同步改变。这样继续传递下去才不会导致 NestMenu 接收到的数据混乱。 <template><nest-menu:data="data":defaultActiveIds="ids"@change="activeIdsChange"/></template> NestMenu 初始化的时候,对有默认值的情况做一下处理,优先使用数组中取到的 id 值。 setup(props:IProps,context){const{depth=0,activeIds}=props;/***这里activeIds也可能是异步获取到的所以用watch保证初始化*/constactiveId=ref<number|null|undefined>(null);watch(()=>activeIds,(newActiveIds)=>{if(newActiveIds){constnewActiveId=newActiveIds[depth];if(newActiveId){activeId.value=newActiveId;}}},{immediate:true,});} 这样,如果 activeIds 数组中取不到的话,默认还是 null,在 watch 到菜单数据变化的逻辑中,如果 activeId 是 null 的话,会被初始化为第一个子菜单的 id。 watch(()=>props.data,(newData)=>{if(!activeId.value){if(newData&&newData.length){activeId.value=newData[0].id}}},{immediate:true,}) 在最外层页面容器监听到 change 事件的时候,要把数据源同步一下: <template><nest-menu:data="data":activeIds="ids"@change="activeIdsChange"/></template><script>import{ref}from"vue";exportdefault{name:"App",setup(){constactiveIdsChange=(newIds)=>{ids.value=newIds;};return{ids,activeIdsChange,};},};</script> 如此一来,外部传入 activeIds 的时候,就可以控制整个 NestMenu 的高亮选中逻辑了。 数据源变动引发的 bug。 这时候,面试官对着你的 App 文件稍作改动,然后演示了这样一个 bug: App.vue 的 setup 函数中加了这样的一段逻辑: onMounted(()=>{setTimeout(()=>{menu.value=[data[0]].slice()},1000)}) 也就是说,组件渲染完成后过了一秒,菜单的最外层只剩下一项了,这时候面试官在一秒之内点击了最外层的第二项,这个组件在数据源改变之后,会报错: 这是因为数据源已经改变了,但是组件内部的 activeId 状态依然停留在了一个已经不存在了的 id 上。 这会导致 subMenu 这个 computed 属性在计算时出错。 我们对 watch data 观测数据源的这段逻辑稍加改动: watch(()=>props.data,(newData)=>{if(!activeId.value){if(newData&&newData.length){activeId.value=newData[0].id}}//如果当前层级的data中遍历无法找到`activeId`的值说明这个值失效了//把它调整成数据源中第一个子菜单项的idif(!props.data.find(({id})=>id===activeId.value)){activeId.value=props.data?.[0].id}},{immediate:true,//在观测到数据变动之后同步执行这样会防止渲染发生错乱flush:'sync',}) 注意这里的 flush: "sync" 很关键,Vue3 对于 watch 到数据源变动之后触发 callback 这一行为,默认是以 post 也就是渲染之后再执行的,但是在当前的需求下,如果我们用错误的 activeId 去渲染,就会直接导致报错了,所以我们需要手动把这个 watch 变成一个同步行为。 这下再也不用担心数据源变动导致渲染错乱了。 完整代码 App.vue <template><nest-menu:data="data":activeIds="ids"@change="activeIdsChange"/></template><script>import{ref}from"vue";importNestMenufrom"./components/NestMenu.vue";importdatafrom"./menu.js";import{getSubIds}from"./util";exportdefault{name:"App",setup(){//假设默认选中id为7constactiveId=7;constfindPath=(menus,targetId)=>{letids;consttraverse=(subMenus,prev)=>{if(ids){return;}if(!subMenus){return;}subMenus.forEach((subMenu)=>{if(subMenu.id===activeId){ids=[...prev,activeId];return;}traverse(subMenu._child,[...prev,subMenu.id]);});};traverse(menus,[]);returnids;};constids=ref(findPath(data,activeId));constactiveIdsChange=(newIds)=>{ids.value=newIds;console.log("当前选中的id路径",newIds);};return{ids,activeIdsChange,data,};},components:{NestMenu,},};</script> NestMenu.vue <template><divclass="wrap"><divclass="menu-wrap":class="`menu-wrap-${depth}`"><divclass="menu-item"v-for="menuItemindata":class="getActiveClass(menuItem.id)"@click="onMenuItemClick(menuItem)":key="menuItem.id">{{menuItem.name}}</div></div><nest-menu:key="activeId"v-if="subMenu&&subMenu.length":data="subMenu":depth="depth+1":activeIds="activeIds"@change="onSubActiveIdChange"></nest-menu></div></template><scriptlang="ts">import{watch,ref,onMounted,computed}from"vue";importdatafrom"../menu";interfaceIProps{data:typeofdata;depth:number;activeIds?:number[];}exportdefault{name:"NestMenu",props:["data","depth","activeIds"],setup(props:IProps,context){const{depth=0,activeIds,data}=props;/***这里activeIds也可能是异步获取到的所以用watch保证初始化*/constactiveId=ref<number|null|undefined>(null);watch(()=>activeIds,(newActiveIds)=>{if(newActiveIds){constnewActiveId=newActiveIds[depth];if(newActiveId){activeId.value=newActiveId;}}},{immediate:true,flush:'sync'});/***菜单数据源发生变化的时候默认选中当前层级的第一项*/watch(()=>props.data,(newData)=>{if(!activeId.value){if(newData&&newData.length){activeId.value=newData[0].id;}}//如果当前层级的data中遍历无法找到`activeId`的值说明这个值失效了//把它调整成数据源中第一个子菜单项的idif(!props.data.find(({id})=>id===activeId.value)){activeId.value=props.data?.[0].id;}},{immediate:true,//在观测到数据变动之后同步执行这样会防止渲染发生错乱flush:"sync",});constonMenuItemClick=(menuItem)=>{constnewActiveId=menuItem.id;if(newActiveId!==activeId.value){activeId.value=newActiveId;constchild=getActiveSubMenu();constsubIds=getSubIds(child);//把子菜单的默认第一项ids也拼接起来向父组件emitcontext.emit("change",[newActiveId,...subIds]);}};/***接受到子组件更新activeId的同时*需要作为一个中介告知父组件activeId更新了*/constonSubActiveIdChange=(ids)=>{context.emit("change",[activeId.value].concat(ids));};constgetActiveSubMenu=()=>{returnprops.data?.find(({id})=>id===activeId.value)._child;};constsubMenu=computed(getActiveSubMenu);/***样式相关*/constgetActiveClass=(id)=>{if(id===activeId.value){return"menu-active";}return"";};/***递归收集子菜单第一项的id*/constgetSubIds=(child)=>{constsubIds=[];consttraverse=(data)=>{if(data&&data.length){constfirst=data[0];subIds.push(first.id);traverse(first._child);}};traverse(child);returnsubIds;};return{depth,activeId,subMenu,onMenuItemClick,onSubActiveIdChange,getActiveClass,};},};</script><style>.wrap{padding:12px0;}.menu-wrap{display:flex;flex-wrap:wrap;}.menu-wrap-0{background:#ffccc7;}.menu-wrap-1{background:#fff7e6;}.menu-wrap-2{background:#fcffe6;}.menu-item{margin-left:16px;cursor:pointer;white-space:nowrap;}.menu-active{color:#f5222d;}</style> 源码地址 https://github.com/sl1673495/vue-nested-menu 总结 一个递归的菜单组件,说简单也简单,说难也有它的难点。如果我们不理解 Vue 的异步渲染和观察策略,可能中间的 bug 就会困扰我们许久。所以适当学习原理还是挺有必要的。 在开发通用组件的时候,一定要注意数据源的传入时机(同步、异步),对于异步传入的数据,要利用好 watch 这个 API 去观测变动,做相应的操作。并且要考虑数据源的变化是否会和组件内原来保存的状态冲突,在适当的时机要做好清理操作。 另外留下一个小问题,我在 NestMenu 组件 watch 数据源的时候,选择这样去做: watch((()=>props.data); 而不是解构后再去观测: const{data}=props;watch(()=>data); 这两者之间有区别吗?这又是一道考察深度的面试题。 开发优秀组件的路还是很漫长的,欢迎各位也在评论区留下你的看法~ 招聘 字节跳动内推啦,Client Infrastructure是字节跳动终端基础架构团队,面向字节跳动全业务线的移动端、Web、Desktop等终端业务的基础架构部门,为公司业务的高效迭代、质量保证、研发效率和体验提供平台、工具、框架和专项技术能力支撑。研发领域包括但不限于APP框架和基础组件、研发体系、自动化测试、APM、跨平台框架、端智能解决方案、Web开发引擎、Node.js基建以及下一代移动开发技术的预研等,目前在北上广深杭五地均设有研发中心。 上海的同学点这里一键投递,来我们部门和我做同事吧~ https://job.toutiao.com/s/JhRtmQv 其他地区(北上广深杭)也可以自己搜索你想要的业务线和工作地点,通过我的下方内推链接直接投递即可。 https://job.toutiao.com/s/JhRDWep 校招的同学看这里: 投递链接: https://job.toutiao.com/s/JhRV7nC ❤️ 感谢大家 1.如果本文对你有帮助,就点个赞支持下吧,你的「赞」是我创作的动力。 2.关注公众号「前端从进阶到入院」即可加我好友,我拉你进「前端进阶交流群」,大家一起共同交流和进步。 参考资料 [1]Github Pages: https://sl1673495.github.io/vue-nested-menu/ 本文分享自微信公众号 - 前端从进阶到入院(code_with_love)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

Java 学习(25)---(IO流之数据流 / 内存操作流 / 打印流 / 标准输入输出流)

数据流 可以读写基本数据类型的数据 数据输入流: DataInputStream DataInputStream(InputStream in) 数据输出流: DataOutputStream DataOutputStream(OutputStream out) privatestaticvoid read() throws IOException { // DataInputStream(InputStream in) // 创建数据输入流对象 DataInputStream dis = newDataInputStream( new FileInputStream("dos.txt")); // 读数据 byte b = dis.readByte(); short s = dis.readShort(); int i = dis.readInt(); long l = dis.readLong(); float f = dis.readFloat(); double d = dis.readDouble(); char c = dis.readChar(); boolean bb = dis.readBoolean(); // 释放资源 dis.close(); } privatestaticvoid write() throws IOException { // DataOutputStream(OutputStream out) // 创建数据输出流对象 DataOutputStream dos = new DataOutputStream(new FileOutputStream( "dos.txt" )); // 写数据了 dos.writeByte(10); dos.writeShort(100); dos.writeInt(1000); dos.writeLong(10000); dos.writeFloat(12.34F); dos.writeDouble(12.56); dos.writeChar('a'); dos.writeBoolean(true); // 释放资源 dos.close(); } 内存操作流 用于处理临时存储信息的,程序结束,数据就从内存中消失。 字节数组:ByteArrayInputStream ByteArrayOutputStream 字符数组:CharArrayReader CharArrayWriter 字符串:StringReader StringWriter // ByteArrayOutputStream() ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 写数据 for ( int x = 0; x < 10; x++) { baos.write(("hello" +x ).getBytes()); } // 释放资源 // 通过查看源码我们知道这里什么都没做,所以根本不需要 close() // baos.close(); // public byte[] toByteArray() byte []bys = baos.toByteArray(); // 读数据 // ByteArrayInputStream(byte[] buf) ByteArrayInputStream bais = new ByteArrayInputStream(bys); int by = 0; while((by = bais.read()) != -1) { System. out .print((char) by ); } // bais.close(); 打印流 (1)字节打印流,字符打印流 (2)特点: A:只操作目的地 ,不操作数据源 B:可以操作任意类型的数据 C:如果启用了自动刷新,在调用 println() 方法的时候,能够换行并刷新 D:可以直接操作文件 问题:哪些流可以直接操作文件呢 ? 看API,如果其构造方法能够同时接收 File和String类型的参数,一般都是可以直接操作文件的 (3)案例:复制文本文件 BufferedReaderbr = new BufferedReader(newFileReader("a.txt")); PrintWriter pw = new PrintWriter(new FileWriter("b.txt"),true); Stringline= null ; while ((line=br.readLine())!=null ) { pw.println(line ); } pw.close(); br.close(); 标准输入输出流 ( 理解即可) System类下面有这样的两个字段:in标准输入流 /out 标准输出流 (2)三种键盘录入方式 A:main 方法的 args 接收参数 B:System.in通过 BufferedReader 进行包装 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); C:Scanner Scanner sc = new Scanner(System.in); (3)输出语句的原理和如何使用字符流输出数据 A:原理 System.out.println("helloworld"); PrintStream ps = System.out; ps.println("helloworld"); B:把 System.out 用字符缓冲流包装一下使用 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out)); ​​​​​​

优秀的个人博客,低调大师

云栖科技评论第25期:德州扑克人机大战落幕 人工智能赢了176万美元

1、德州扑克人机大战落幕:人工智能赢了176万美元 德州扑克人机大战落幕:人工智能赢了176万美元 【新闻摘要】美国时间1月30日,由卡耐基梅隆大学开发的人工智能系统Libratus战胜了四位人类德州扑克顶级选手,获得最终胜利。本次赛事历时20天,人工智能系统每天均取得获胜战绩,共赢得176.6万美元。每个牌局结束后,位于匹兹堡计算中心内部的超级计算机,通过对系统进行优化,提高白天比赛中的对垒策略。系统在600-700个节点上运行,通过大规模内存计算实现赛程中的实时决策应答。参与赛事的一位选手在赛事过半时直言:“人类已经没有真正获胜的机会。”新闻链接【小云评论】自去年AlphaGo进军围棋界后,人工智能与人类顶级高手的较量已成为热点。与围棋不同,在德州扑克中单一玩家并不掌握牌局的全部信息,需要通过猜测分析出其他选手的执牌与出牌策略。

优秀的个人博客,低调大师

9月25日云栖精选夜读:阿里云发布自研商用关系型数据库POLARDB

本文讲的是阿里云发布自研商用关系型数据库POLARDB,在企业数据容量环式增长时代,数据库容量小、存储空间扩展缓慢、性能不足,以及扩容升级慢等问题渐显,传统数据库显然已难以支撑诸如物联网、新金融、新零售、新制造、电信等高吞吐场景业务的快速发展。 热点热议 阿里云发布自研商用关系型数据库POLARDB 作者:yq传送门 6倍性能差100TB容量,阿里云POLARDB如何实现? 作者:yq传送门 数据库诞生40年,阿里云AWS用技术推动第三次变革 作者:yq传送门 知识整理 MarkDown基本语法 作者:java小萌新 c++ const的各种用法 作者:茶花盛开 解决json转换问题:net.sf.ezmorph.bean.MorphDynaBean cannotbe cast to xxx 作者:落葉知秋 vue.js与其他前端框架的对比 作者:code_xzh 20个非常有用的Java程序片段 作者:茶花盛开 美文回顾 基于 Laravel 开发 ThinkSNS+ 中前端的抉择 作者:thinksns 数据分析:让Python告诉你约会是否可以成功 作者:数据分析啊 解析Dataworks中的运行和测试运行的区别 作者:浪丶彭 学术界关于HBase在物联网/车联网/互联网/金融/高能物理等八大场景的理论研究 作者:封神 发表在:阿里云数据库ApsaraDB mongodb查询处理流程 作者:yanglm_28 PostgreSQL GUC 参数级别介绍 作者:德哥 发表在:阿里云数据库ApsaraDB 如何加快C++代码的编译速度 作者:茶花盛开 往期精选回顾 9月22日云栖精选夜读:脑洞 | 横扫围棋界的AlphaGo竟然出纪录片了!介意剧透者慎点… 9月21日云栖精选夜读:专访新浪微博黄波:千人千面,机器学习赋能用户信息流消费 9月20日云栖精选夜读:异构计算&高性能计算分论坛——揭秘拿什么实现超算平民化、国际化? 9月19日云栖精选夜读:阿里云MVP Meetup极客开跑,与MVP一起玩转云计算 9月18日云栖精选夜读:「阿里巴巴编码规范(Java版)」认证考试出炉!你考过了吗?

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册