整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

关于ExtJS与JQuery对比

先分别介绍一下ExtJS和JQuery,然后进行对比分析

一、什么是ExtJS?

1、ExtJS可以用来开发RIA也即富客户端的AJAX应用,是一个用javascript写的,主要用于创建前端用户界面,是一个与后台技术无关的前端ajax框架。因此,可以把ExtJS用在.Net、Java、Php等各种开发语言开发的应用中。ExtJs最开始基于YUI技术,由开发人员JackSlocum开发,通过参考JavaSwing等机制来组织可视化组件,无论从UI界面上CSS样式的应用,到数据解析上的异常处理,都可算是一款不可多得的JavaScript客户端技术的精品。

2、Ext的UI组件模型和开发理念脱胎、成型于Yahoo组件库YUI和Java平台上Swing两者,并为开发者屏蔽了大量跨浏览器方面的处理。相对来说,EXT要比开发者直接针对DOM、W3C对象模型开发UI组件轻松。

二、什么是JQuery?

jQuery是一个兼容多浏览器的javascript框架,核心理念是write less,do more(写得更少,做得更多)。jQuery在2006年1月由美国人John Resig在纽约的barcamp发布,吸引了来自世界各地的众多JavaScript高手加入,由Dave Methvin率领团队进行开发。如今,jQuery已经成为最流行的javascript框架,在世界前10000个访问最多的网站中,有超过55%在使用jQuery。

jQuery是免费、开源的,使用MIT许可协议。jQuery的语法设计可以使开发者更加便捷,例如操作文档对象、选择DOM元素、制作动画效果、事件处理、使用Ajax以及其他功能。除此以外,jQuery提供API让开发者编写插件。其模块化的使用方式使开发者可以很轻松的开发出功能强大的静态或动态网页。

三、二者对比

1.JQuery-EasyUI是仿照Ext做的。

2.Ext框架是一个整体,面向对象的编程思想,每个控件之间可以相互通讯。

3.JQuery的控件全都是分散的,没有整体性可言。你可以单独拿出来一个控件就能用。

4.若要开发系统应用,首选Ext,控件库丰富,扩展和维护都方便。若是简单的页面动画和效果,首选JQuery

5.jquery只是一个工具库,比较简单,相对容易。 Ext是一套真正的ria开发框架,甚至可以实现桌面应用一样的强大功能。本身代码质量极高,而且是高度的面向对象设计。jQuery 入门相对容易一些,实际用的过程中要用到各种插件,基本上用一个“学”一个。 ExtJS 入门稍难。

6.大小比较:首先ExtJS是一个完整的Framework,是重量级别的,easy ui 是基于jquery库的一套UI组件库,是轻量级的,ExtJS是应用application级的,而jquery是page页面级的。当然application也是由page组成的,那就需要你自己去完成了,考虑你的需求,和使用框架的初衷,选择使用哪一种。同时ExtJs由于是重量级框架,完全面向对象风格,提供API非常完备也非常庞大,所以学习成本也想相对较大。

7.兼容性比较:ExtJS兼容IE全系列浏览器和其他非IE现代浏览器,jquery UI向来不太考虑ie低版本浏览器的兼容,从态度上的鄙视。easy UI是基于jquery的,jquery2.X以上的版本不再支持IE6、7、8,,已郑重声明,请看官方网站,easyUI最新版本1.3.3使jQuery2.0,由于又很多HTML5特性,不再支持IE6,低版本由一些小部分的兼容不够好,请自己做技术选型的时候去测试,你要使用那个版本。在兼容问题上,他们都有瑕疵,看你的接收程度。

8.使用许可license. EXTJS 2.1以上版本,商用需要购买商业授权,jquery UI 使用MIT协议,开源。 jquery easyUI如果商用需遵循license commercial商业许可,也就是要购买使用权.

于html5的Websocket网页即时通讯技术,前端开发采用ExtJS前端框架

JavaEE框架:Mybatis、SpringMVC

先去官网下载ExtJS框架的资料文件:

https://www.sencha.com/products/extjs/evaluate/

可以参考中文翻译过来的官网查看API:

http://extjs-doc-cn.github.io/ext4api/

下载集成的jar:


websocket.css:

@CHARSET "UTF-8";
.l-im-message-warn {
 font-family: "微软雅黑";
 cursor: default;
 width: 100%;
 padding: 5px 0px 5px 25px;
 -webkit-user-select : none;
 background: url("../images/information.png") no-repeat 5;
}
.l-im-message {
 font-family: "微软雅黑";
 cursor: default;
 width: 100%;
}
.l-im-message-over {
 background-color: rgba(233, 233, 233, 0.5);
}
.l-im-message-selected {
 background-color: rgba(250, 218, 90, 0.5);
}
.l-im-message-header {
 font-size: 12px;
 padding: 5px 0px 5px 10px;
}
.l-im-message-header-self {
 color: green;
}
.l-im-message-header-remote {
 color: blue;
}
.l-im-message-body {
 font-size: 12px;
 padding: 2px 0px 2px 20px;
}
.user-win {
 background-image: url( ../images/user_win.png )
 !important;
}
.user-online {
 background-image: url( ../images/group.png )
 !important;
}
.user {
 background-image: url( ../images/user.gif )
 !important;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

websocket.js:

var websocket;
var isCreatw = false;
var title="";
var win;
var input;
var isQj = true;
var toUser="";
function toUserMsg(toU){
 if((!isQj && toUser == toU) || toU == user){
 win.setTitle(title + " (已连接) 【现在全局对话】");
 isQj = true;
 toUser = "";
 }else{
 win.setTitle(title + " (已连接) 【现在单独与"+toU+"对话】");
 isQj = false;
 toUser = toU;
 }
}
 function creatw() {
 if(isCreatw){
 alert("已经启动");
 return;
 }else{
 isCreatw = true;
 }
 //创建用户输入框
 input = Ext.create('Ext.form.field.HtmlEditor', {
 region : 'south',
 height : 120,
 enableFont : false,
 enableSourceEdit : false,
 enableAlignments : false,
 listeners : {
 initialize : function() {
 Ext.EventManager.on(me.input.getDoc(), {
 keyup : function(e) {
 if (e.ctrlKey === true
 && e.keyCode == 13) {
 e.preventDefault();
 e.stopPropagation();
 send();
 }
 }
 });
 }
 }
 });
 //创建消息展示容器
 var output = Ext.create('MessageContainer', {
 region : 'center'
 });
 var dialog = Ext.create('Ext.panel.Panel', {
 region : 'center',
 layout : 'border',
 items : [input, output],
 buttons : [{
 text : '发送',
 handler : send
 }]
 });
 //初始话WebSocket
 function initWebSocket() {
 if (window.WebSocket) {
 websocket = new WebSocket(encodeURI('ws://'+wimadress));
 websocket.onopen = function() {
 //连接成功
 win.setTitle(title + ' (已连接) 【现在全局对话】');
 websocket.send('admin'+user);
 }
 websocket.onerror = function() {
 //连接失败
 win.setTitle(title + ' (连接发生错误)');
 }
 websocket.onclose = function() {
 //连接断开
 win.setTitle(title + ' (已经断开连接)');
 }
 //消息接收
 websocket.onmessage = function(message) {
 var message = JSON.parse(message.data);
 //接收用户发送的消息
 if (message.type == 'message') {
 output.receive(message);
 } else if (message.type == 'get_online_user') {
 //获取在线用户列表
 var root = onlineUser.getRootNode();
 Ext.each(message.list,function(user){
 var node = root.createNode({
 id : user,
 text : user,
 iconCls : 'user',
 leaf : true
 });
 root.appendChild(node);
 });
 } else if (message.type == 'user_join') {
 //用户上线
 var root = onlineUser.getRootNode();
 var user = message.user;
 var node = root.createNode({
 id : user,
 text : user,
 iconCls : 'user',
 leaf : true
 });
 root.appendChild(node);
 } else if (message.type == 'user_leave') {
 //用户下线
 var root = onlineUser.getRootNode();
 var user = message.user;
 var node = root.findChild('id',user);
 root.removeChild(node);
 }
 }
 }
 };
 //在线用户树
 var onlineUser = Ext.create('Ext.tree.Panel', {
 title : '在线用户',
 rootVisible : false,
 region : 'east',
 width : 150,
 lines : false,
 useArrows : true,
 autoScroll : true,
 split : true,
 iconCls : 'user-online',
 store : Ext.create('Ext.data.TreeStore', {
 root : {
 text : '在线用户',
 expanded : true,
 children : []
 }
 })
 });
 title = '欢迎您:' + user;
 //展示窗口
 win = Ext.create('Ext.window.Window', {
 title : title + ' (未连接)',
 layout : 'border',
 iconCls : 'user-win',
 minWidth : 650,
 minHeight : 460,
 width : 650,
 animateTarget : 'websocket_button',
 height : 460,
 items : [dialog,onlineUser],
 border : false,
 listeners : {
 render : function() {
 initWebSocket();
 }
 }
 });
 win.show();
 win.on("close",function(){
 websocket.send('LeaveAdmin');
 isCreatw = false;
 });
 //发送消息
 function send() {
 var content = input.getValue();
 if(toUser != ""){content = "admin886"+toUser+"admin888" + content;}
 var message = {};
 if (websocket != null) {
 if (input.getValue()) {
 Ext.apply(message, {
 from : user,
 content : content,
 timestamp : new Date().getTime(),
 type : 'message'
 });
 websocket.send(JSON.stringify(message));
 //output.receive(message);
 input.setValue('');
 }
 } else {
 Ext.Msg.alert('提示', '您已经掉线,无法发送消息!');
 }
 }
};
//用于展示用户的聊天信息
Ext.define('MessageContainer', {
 extend : 'Ext.view.View',
 trackOver : true,
 multiSelect : false,
 itemCls : 'l-im-message',
 itemSelector : 'div.l-im-message',
 overItemCls : 'l-im-message-over',
 selectedItemCls : 'l-im-message-selected',
 style : {
 overflow : 'auto',
 backgroundColor : '#fff'
 },
 tpl : [
 '<div class="l-im-message-warn">​欢迎使用即时通讯系统。</div>',
 '<tpl for=".">',
 '<div class="l-im-message">',
 '<div class="l-im-message-header l-im-message-header-{source}">{from} {timestamp}</div>',
 '<div class="l-im-message-body">{content}</div>', '</div>',
 '</tpl>'],
 messages : [],
 initComponent : function() {
 var me = this;
 me.messageModel = Ext.define('Leetop.im.MessageModel', {
 extend : 'Ext.data.Model',
 fields : ['from', 'timestamp', 'content', 'source']
 });
 me.store = Ext.create('Ext.data.Store', {
 model : 'Leetop.im.MessageModel',
 data : me.messages
 });
 me.callParent();
 },
 //将服务器推送的信息展示到页面中
 receive : function(message) {
 var me = this;
 message['timestamp'] = Ext.Date.format(new Date(message['timestamp']),
 'H:i:s');
 if(message.from == user){
 message.source = 'self';
 }else{
 message.source = 'remote';
 }
 me.store.add(message);
 if (me.el.dom) {
 me.el.dom.scrollTop = me.el.dom.scrollHeight;
 }
 }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254

业务代码编写:

ChatServer.java

package com.appms.websocket;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Date;
import net.sf.json.JSONObject;
import org.java_websocket.WebSocket;
import org.java_websocket.WebSocketImpl;
import org.java_websocket.framing.Framedata;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
/**
 * 即时通讯
 */
public class ChatServer extends WebSocketServer{
 public ChatServer(int port) throws UnknownHostException {
 super(new InetSocketAddress(port));
 }
 public ChatServer(InetSocketAddress address) {
 super(address);
 }
 /**
 * 触发连接事件
 */
 @Override
 public void onOpen( WebSocket conn, ClientHandshake handshake ) {
 }
 /**
 * 触发关闭事件
 */
 @Override
 public void onClose( WebSocket conn, int code, String reason, boolean remote ) {
 userLeave(conn);
 }
 /**
 * 客户端发送消息到服务器时触发事件
 */
 @Override
 public void onMessage(WebSocket conn, String message){
 message = message.toString();
 if(null != message && message.startsWith("admin")){
 this.userjoin(message.replaceFirst("admin", ""),conn);
 }if(null != message && message.startsWith("LeaveAdmin")){
 this.userLeave(conn);
 }if(null != message && message.contains("admin886")){
 String toUser = message.substring(message.indexOf("admin886")+8, message.indexOf("admin888"));
 message = message.substring(0, message.indexOf("admin886")) +"[私信] "+ message.substring(message.indexOf("admin888")+8, message.length());
 ChatServerPool.sendMessageToUser(ChatServerPool.getWebSocketByUser(toUser),message);//向所某用户发送消息
 ChatServerPool.sendMessageToUser(conn, message);//同时向本人发送消息
 }else{
 ChatServerPool.sendMessage(message.toString());//向所有在线用户发送消息
 }
 }
 public void onFragment( WebSocket conn, Framedata fragment ) {
 }
 /**
 * 触发异常事件
 */
 @Override
 public void onError( WebSocket conn, Exception ex ) {
 ex.printStackTrace();
 if( conn != null ) {
 //some errors like port binding failed may not be assignable to a specific websocket
 }
 }
 /**
 * 用户加入处理
 * @param user
 */
 public void userjoin(String user, WebSocket conn){
 JSONObject result = new JSONObject();
 result.element("type", "user_join");
 result.element("user", "<a onclick=\"toUserMsg('"+user+"');\">"+user+"</a>");
 ChatServerPool.sendMessage(result.toString()); //把当前用户加入到所有在线用户列表中
 String joinMsg = "{\"from\":\"[系统]\",\"content\":\""+user+"上线了\",\"timestamp\":"+new Date().getTime()+",\"type\":\"message\"}";
 ChatServerPool.sendMessage(joinMsg); //向所有在线用户推送当前用户上线的消息
 result = new JSONObject();
 result.element("type", "get_online_user");
 ChatServerPool.addUser(user,conn); //向连接池添加当前的连接对象
 result.element("list", ChatServerPool.getOnlineUser());
 ChatServerPool.sendMessageToUser(conn, result.toString()); //向当前连接发送当前在线用户的列表
 }
 /**
 * 用户下线处理
 * @param user
 */
 public void userLeave(WebSocket conn){
 String user = ChatServerPool.getUserByKey(conn);
 boolean b = ChatServerPool.removeUser(conn); //在连接池中移除连接
 if(b){
 JSONObject result = new JSONObject();
 result.element("type", "user_leave");
 result.element("user", "<a onclick=\"toUserMsg('"+user+"');\">"+user+"</a>");
 ChatServerPool.sendMessage(result.toString()); //把当前用户从所有在线用户列表中删除
 String joinMsg = "{\"from\":\"[系统]\",\"content\":\""+user+"下线了\",\"timestamp\":"+new Date().getTime()+",\"type\":\"message\"}";
 ChatServerPool.sendMessage(joinMsg); //向在线用户发送当前用户退出的消息
 }
 }
 public static void main( String[] args ) throws InterruptedException , IOException {
 WebSocketImpl.DEBUG = false;
 int port = 8887; //端口
 ChatServer s = new ChatServer(port);
 s.start();
 System.out.println( "服务器的端口" + s.getPort() );
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

ChatServerPool.java:

package com.appms.websocket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.java_websocket.WebSocket;
/**
 * 即时通讯
 */
public class ChatServerPool {
 private static final Map<WebSocket,String> userconnections = new HashMap<WebSocket,String>();
 /**
 * 获取用户名
 * @param session
 */
 public static String getUserByKey(WebSocket conn){
 return userconnections.get(conn);
 }
 /**
 * 获取WebSocket
 * @param user
 */
 public static WebSocket getWebSocketByUser(String user){
 Set<WebSocket> keySet = userconnections.keySet();
 synchronized (keySet) {
 for (WebSocket conn : keySet) {
 String cuser = userconnections.get(conn);
 if(cuser.equals(user)){
 return conn;
 }
 }
 }
 return null;
 }
 /**
 * 向连接池中添加连接
 * @param inbound
 */
 public static void addUser(String user, WebSocket conn){
 userconnections.put(conn,user); //添加连接
 }
 /**
 * 获取所有的在线用户
 * @return
 */
 public static Collection<String> getOnlineUser(){
 List<String> setUsers = new ArrayList<String>();
 Collection<String> setUser = userconnections.values();
 for(String u:setUser){
 setUsers.add("<a onclick=\"toUserMsg('"+u+"');\">"+u+"</a>");
 }
 return setUsers;
 }
 /**
 * 移除连接池中的连接
 * @param inbound
 */
 public static boolean removeUser(WebSocket conn){
 if(userconnections.containsKey(conn)){
 userconnections.remove(conn); //移除连接
 return true;
 }else{
 return false;
 }
 }
 /**
 * 向特定的用户发送数据
 * @param user
 * @param message
 */
 public static void sendMessageToUser(WebSocket conn,String message){
 if(null != conn && null != userconnections.get(conn)){
 conn.send(message);
 }
 }
 /**
 * 向所有的用户发送消息
 * @param message
 */
 public static void sendMessage(String message){
 Set<WebSocket> keySet = userconnections.keySet();
 synchronized (keySet) {
 for (WebSocket conn : keySet) {
 String user = userconnections.get(conn);
 if(user != null){
 conn.send(message);
 }
 }
 }
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105

写个过滤器,在项目执行时启动:

package com.appms.filter;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.Calendar;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.java_websocket.WebSocketImpl;
import com.appms.base.BaseController;
import com.appms.base.Const;
import com.appms.utils.Tools;
import com.appms.websocket.ChatServer;
import com.appms.websocket.OnlineChatServer;
public class StartFilter extends BaseController implements Filter{
 /**
 * 初始化
 */
 public void init(FilterConfig fc) throws ServletException {
 this.startWebsocketInstantMsg();
 this.startWebsocketOnline();
 }
 /**
 * 启动即时聊天服务
 */
 public void startWebsocketInstantMsg(){
 WebSocketImpl.DEBUG = false;
 ChatServer s = null;
 try {
 String strWEBSOCKET = Tools.readTxtFile(Const.WEBSOCKET);//读取WEBSOCKET配置,获取端口配置
 if(null != strWEBSOCKET && !"".equals(strWEBSOCKET)){
 String strIW[] = strWEBSOCKET.split(",fh,");
 if(strIW.length == 4){
 s = new ChatServer(Integer.parseInt(strIW[1]));
 s.start();
 }
 }
 System.out.println( "websocket服务器启动,端口" + s.getPort() );
 } catch (UnknownHostException e) {
 e.printStackTrace();
 }
 }
 /**
 * 启动在线管理服务
 */
 public void startWebsocketOnline(){
 WebSocketImpl.DEBUG = false;
 OnlineChatServer s = null;
 try {
 String strWEBSOCKET = Tools.readTxtFile(Const.WEBSOCKET);//读取WEBSOCKET配置,获取端口配置
 if(null != strWEBSOCKET && !"".equals(strWEBSOCKET)){
 String strIW[] = strWEBSOCKET.split(",fh,");
 if(strIW.length == 4){
 s = new OnlineChatServer(Integer.parseInt(strIW[3]));
 s.start();
 }
 }
 System.out.println( "websocket服务器启动,端口" + s.getPort() );
 } catch (UnknownHostException e) {
 e.printStackTrace();
 }
 }
 //计时器
 public void timer() {
 Calendar calendar = Calendar.getInstance();
 calendar.set(Calendar.HOUR_OF_DAY, 9); // 控制时
 calendar.set(Calendar.MINUTE, 0); // 控制分
 calendar.set(Calendar.SECOND, 0); // 控制秒
 Date time = calendar.getTime(); // 得出执行任务的时间
 Timer timer = new Timer();
 timer.scheduleAtFixedRate(new TimerTask() {
 public void run() {
 //PersonService personService = (PersonService)ApplicationContext.getBean("personService");
 }
 }, time, 1000*60*60*24);// 这里设定将延时每天固定执行
 }
 public void destroy() {
 // TODO Auto-generated method stub
 }
 public void doFilter(ServletRequest arg0, ServletResponse arg1,
 FilterChain arg2) throws IOException, ServletException {
 // TODO Auto-generated method stub
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109

在web.xml里配置:

 <filter>
 <filter-name>startFilter</filter-name>
 <filter-class>com.appms.filter.StartFilter</filter-class>
 </filter>
1
2
3
4

在jsp页面进行调用:

 <script type="text/javascript">var wimadress="127.0.0.1:8887";</script>
 <script type="text/javascript">var oladress="127.0.0.1:8889";</script>
 <link rel="stylesheet" type="text/css" href="plugins/websocket/ext4/resources/css/ext-all.css">
 <link rel="stylesheet" type="text/css" href="plugins/websocket/css/websocket.css" />
 <script type="text/javascript" src="plugins/websocket/ext4/ext-all-debug.js"></script>
 <script type="text/javascript" src="plugins/websocket/websocket.js"></script>
 <!--引入属于此页面的js -->
 <script type="text/javascript" src="source/js/jquery-1.8.3.js"></script>
1
2
3
4
5
6
7
8
9

点击li标签跳出聊天页面

ul class="am-avg-sm-1 am-avg-md-4 am-margin am-padding am-text-center admin-content-list ">
 <li onclick="creatw();"><a href="javascript:;"><span class="am-icon-btn am-icon-file-text"></span><br/>即时通讯<br/></a></li>
 <li><a href="fusioncharts/index.do" class="am-text-warning"><span class="am-icon-btn am-icon-briefcase"></span><br/>图表统计<br/></a></li>
 <li><a href="#" class="am-text-danger"><span class="am-icon-btn am-icon-recycle"></span><br/>昨日访问<br/>80082</a></li>
 <li><a href="#" class="am-text-secondary"><span class="am-icon-btn am-icon-user-md"></span><br/>在线用户<br/>3000</a></li>
 </ul>
1
2
3
4
5
6

私聊:


群聊:


基于ExtJS前端框架的Websocket即时通讯系统


里妹导读:今年的双11已经是阿里资深前端技术专家舒文来阿里的第11年,从应届生到双11前端PM,他一路升级打怪,实现了岗位上从P4到P9的晋升。这第11届双11顺利结束之际,他把在阿里这些年的成长经历做一个总结和分享,希望你能在他的故事中得到些许启发。

作者简介:舒文,来自淘系技术部前端团队。目前负责淘系(淘宝+天猫)的营销活动、互动的业务,也在阿里巴巴前端委员会主导搭建体系的技术方向。

P4:懵懂学生入行记 (2008.10-2009.12)

2008年10月,入职阿里巴巴日文站,以前端岗加入UED团队。日文站的业务源于阿里巴巴国际站 - 提供在线的平台化服务,专注撮合中日贸易。

在那个前端的鸿蒙年代,页面重构工程师和Javascript程序员还是两个细分职位: 在阿里,前端、交互、视觉共同划属用户体验部门。

而在部份公司,有专门的重构工程师。

在日文站亦如是:现实情况是杭州和东京的团队,具备写JS能力的工程师均不多。不少同事专注在HTML/CSS领域,且专研极深。举个例子:在日文站,所有的HTML/CSS代码必须通过两个Lint工具:HTML文書の文法をチェックし、採点します、[The W3C Markup Validation - 这让代码的规范性更好外,也能获得更好的搜索引擎分析&索引权重。

随着业务发展,需求场景多且复杂起来,对Javascript开发能力要求就越高。同时,也激发了我的技术热情。除了看书,我迫切想参与其他事业部的前端交流会。感谢时任主管的支持,我开始主动跨部门沟通,将他山之玉和学习所得有了更多结合。慢慢地,在团队内部提出一些技术方案并逐渐应用到杭州/东京两个团队 (特别感谢最早时代的D2)。

在那个阶段,我几乎参与了所有较为复杂的项目。结合当年的热门大作《高性能网站建设指南》、Yahoo网站性能优化黄金法则,负责了主站的性能优化,取得了一些不错结果。

2009年10月获得了日文站优秀员工,年底顺利晋升到新的层级(P5)。

P5:从热血到成长(2010.01-2011.06)

2010年开始,业务略微调整,参与“日本买家获得”(Buyer Growth)。这个业务本质是通过平台技术优化(SEO、SEM等)获得更高的搜索引擎权重、提高Landing到转化。

在这期间,随着被认可度提升、持续的项目历练,我的自信心也得到了锻炼。除了参与更复杂的项目外,我开始主动对业务和技术体系做出一些提案(Proposal) - 包括参考Google Webmaster Guidelines进行技术优化、参与了各种SEO项目,提出了现在回想起来各种或靠谱或不靠谱的提案。非常感谢历任主管的支持和包容,让一个楞头青能够没有后顾之忧地敢想敢做。

日文站初版克隆自Alibaba.com,加之外包参与、跨东京和杭州多团队开发等原因,全站前端框架混杂,包括不限于YUI/Mootools/PrototypeJS/ExtJS/TBra等前端框架&类库混杂在同一业务甚至页面。新人上手、多人协作、性能优化的成本很高,但历史原因积重难返,大家苦不堪言。

结束一个大项目后,时值“愤青”的我对全站的代码做了个调研和分析,说服了主管们,决定启动一个重构项目——旨在将全站混杂的代码统一到 jQuery。并大体确定了执行方案:随业务迭代上线、项目成员1人,我兼任PM、开发/自测。

在之后,一个如同打了鸡血般的前端程序员,白天黑夜的翻历史PRD、熟悉代码逻辑、阅读各种类库的手册,陆续将各业务线的脚本重写了一遍,边做业务项目边重构,整个项目持续了半年多,直到11年Q1完成。

现在回想起来,有些事情真需要一股冲动,正如买房、结婚。如果主管让我再多考虑半天,我兴许就怂了。当然,人生也可能走入另外一条主线。

2011年日文站业务合入B2B,我也参加并通过新事业部的年中晋升(至P6 ) 。


2011年阿里巴巴日文站誓师大会

P6:内部创业 + 前端的无线爆发 (2011.07-2014.06)

两段非常宝贵又特别的经历。

2011年下半年主动选择加入阿里内部的创业项目:打造一款面向个人消费者的云产品。 团队不大,20多人,各路专家云集,各负责人级别都很高。很享受这段工作旅程,我第一次参与桌面Hybrid端项目,第一次开发SPA Web应用,第一次参与跨桌面软件/PC Web/H5的项目,第一次领略敏捷管理的魅力。在来自美国的技术Leader带领下,我接触到很多新领域,极大地开阔了技术视野。

“创业失败是要承担风险的,没有加薪,没有晋升,而且还是996,直到项目成功”。第一次面试这个项目前,业务负责人,阿里工号16的虚竹告诉我。

不算幸运的是,最终这个项目没有取得如预期的成功,业务有了变化和调整。

但幸运的是:这段经历让我亲身体验了一把“创业”,也感受了技术以外的方方面面艰难。这个过程中形成的产品和业务思维,对我日后带来了深刻影响。当然,也在我之后几百次想离职创业时,这段经历让我能够静下心来思考创业的内核 - “为什么要创业?我有什么资源、能做什么事情、创造什么价值?”

忘了说,这段经历之所有特别,还因为办公地点在湖畔花园——马老师家。


湖畔花园办公场地

2012年7月,加入到天猫的前端团队。 主管给予了充分的信任,让我负责天猫H5首页的研发,开始接触淘宝前端—— 一个更成熟完整的技术体系。

2012年底,@三七 加入天猫带领前端团队,作为国内前端领域最早的拓荒者之一,他在随后的两年时间给天猫的前端技术带来体系化的变革——从模块化到工程化再到生产环境的NodeJS,并引入了Mobile First理念。在他的支持下,我带领了一支前端小组陆续参与多条业务线的开发工作。我自己也加入集团级技术建设,主导跨端Web的项目,推进了前端体系的移动化建设。

除专业技术外,花了比原来更多的功夫在团队工作上,包括不限于团队氛围、风格和文化的打造,学会在内网分享中学习借鉴前辈大拿的的管理方法论。在阿里的文化中,非常强调传承。一个人牛不算什么,让一群人厉害才算厉害,最好是让下属比自己还强。

时间花在哪儿,结果就会在哪儿。团队内一些业务尖兵逐渐冒出头来,在后来几年成为天猫甚至大淘系的中坚力量。

这个过程既痛苦又快乐。痛苦在于要做越来越多的超过自己能力层级、远离舒适区的工作,快乐的则是能明显感知到自己在这个过程中的快速成长。

很幸运,在14年年中,顺利通过晋升(P7)。

P7:从双十一中打将出来 (2014.08 - 2016.01)

2014年注定是难忘的一年。在这一年,我开始负责营销活动的业务,并担任2014年双十一前端技术PM工作。

营销活动是一个顶有趣、富有故事的业务:

从技术上:它可以极致简洁到切一个页面上线就行,也可以复杂到如双11一般 ——它是阿里技术的年度大考。

从业务上:它既可以简单到直接把现实生活的促销活动进行虚拟世界的进行流程投射,也能如互联网史上的春运般整合零售生态和供应链。

我和多个团队在天猫制定了各个维度的技术规范,如内控标准、外包规范,有惊无险地渡过了14年双十一。

在PC时代,营销活动的研发模式,对于前端来说,实在过于“简朴”:“5到6位正式前端常年带着数十个外包,根据运营需求开发成百数千的页面,通过一个叫做TMS的运营系统预留坑位给运营同学填数据,交由后端应用推上线(CDN)。” 这类业务活动频次高、页面量大、协同成本高。某种意义上讲,对前端技术挑战并不高,为了更高效的研发,我们能做的是不断提高组件和模块复用率。

感谢TMS,它帮助前端们快速迭代出一个又一个页面,支持了集团的业务,即使在今天,它的设计思路都能称得上精巧犀利。

发生变化来自2014年双十一前,线上发生了一次令我终生难忘的离奇故障(涉及敏感信息,略过细节):当时因临时未排查出原因,VP现场观摩面,我和CDN运维团队的同学们能做的只是不断重启集群应用缓解问题。

我作为时任前端PM,也因技术方案选择失误受到批评责罚。(时隔多年,大家也偶尔调侃,责罚不冤,幸而那天的问题提前暴露而未发生在11.11号当天,否则后果难料 )。

那年双11最终还是完美谢幕,GMV 571亿,移动端成交42.6% ,很多人敏锐的感觉到:移动化时代到来了。

之后技术复盘,我们针对问题做了多轮讨论和推演,决定启动一个代号“斑马”的产品项目:基于我们对营销的理解,运营的实际需求、过往的技术痛点,设计一个高效的页面系统来支持运营快速发布活动上线,更重要的是它是完全的基于Mobile First设计的系统。

正所谓 “一饮一啄,莫非前定” ,因为过去一年的打磨锤炼、痛过后的反思,我们很快把方案框架敲定,并和多个业务搭档共同推进项目实施。

项目于15年初立项,我代表团队“激进”的将阶段目标定为“支撑2015年3月的春季大促” 。这种既要支撑日常高频的大促业务,又要同时从0到1做平台研发,不啻于“飞行中升级引擎”,困难很大。但我背后的思考是:“这就如同创业,没有一往无前的决心和一鼓作气的气势,等想多了细了,多半就怂了怕了”。

我负责整个系统及PM工作,团队培养出来的中坚力量们承担各个功能模块研发。为实现我们心中所定计划,整个项目组如同创业般激情,为了一个共同的目标奋进。春节假期不少同学还在主动提交代码,节后迅速返回公司继续项目。

很幸运,克服了重重困难,“斑马”项目顺利上线,并成功支持了2015年第一个S级大促。基于新的系统,我们共同把营销活动送上新的轨道:因模块化带来的效率提升让我们完全解除了外包依赖、因具备跨端能力运营们可以单次同时进行PC和无线活动、我们设计并推动了CDN构架,单在15年双11就节省了数千万预算。

随着技术迭代,“斑马”这个技术产品,在后面几年成为阿里的基础运营平台之一。

2015年5月,我参与了P8的晋升提名,结果未通过。Leader和我沟通:“成长很快,能力和规划出色,业务成果需要时间验证”。我很坦然接收了建议,晋升答辩中的技术评委的建议中肯不失公正,让我受益很大。

“但行好事,莫问前程”——在随后的时间,“斑马”的能力更为完备。我再次担任了2015年双11的前端PM工作,业务也顺利上线。那一年,双11当天GMV 912亿,移动端成交68%。

16年1月,新主管告诉我,绿色通道通过,晋升至P8。


双十一令牌/虎符

P8:立足业务,敢想敢造 (2016.02 - 2019.07)

一次偶然的机会,看到一张图:如遵循民意苹果手机会变成什么样?给我带来一些对业务和技术的思考:“页面发布系统本质上是一个通用的工具平台:前端/运营/设计师都是它的用户,每个角色都有对它的功能需求。但每个新增功能在解决一类用户需求的同时又增加了平台复杂度,降低的客户的易用性,丢失掉另一类用户的民心。”

“这兴许是一个产品的轮回宿命”,我内心有些敬畏地想。带着这个敬畏,时间到了2017年,随着斑马的功能日趋复杂,我决定将运营操作平台和底层技术能力进行剥离:

  • 将斑马定义为运营系统,专为运营打造极致的页面搭建。
  • 将底层能力抽象成服务平台(代号“天马”),提供开放的研发标准、搭建方案、页面渲染能力。

随着时间推移,“天马”的开放能力不断完善,越来越多的事业部基于它构建面向业务的平台系统,带来的技术回报是:技术复用减少从0到1的研发&硬件成本、标准统一使跨业务协同变为可行。

而在技术的另外一边,业务发生着悄无声息的变化:16年双11GMV 1207亿、17年双11则是1682亿。每年双十一在如此巨大的基数下还能持续业务高增长,个性化算法、推荐技术的大规模应用起到了非常重要的作用。在全局效率上,大数据优于人工干预逐渐得到了共识。在我看来,这些变化意味着时下大家认可的大促会场模式,也将带来新的变化。

2018年2月,我在内部做了个分享,描述了对营销活动未来的预判:

  • 业务化:面向“活动”组织,而非页面。
  • 产品化:构建常态化、心智化的大促产品矩阵,而非单纯是模块级的抽象。
  • 智能化:投放算法化和规则化,而非运营的动态化。

基于以上预判,我和团队的小伙伴,计划联同业务、上下游技术团队共同构建一个面向营销活动的平台产品(项目代号:方舟)。

很感谢历任主管实仙/四虎的信任,愿意帮我顶住重重压力,经过多轮沟通,我们达成了一致共识。

和过去几年稍有不同的是,我逐渐把包括不限于方舟的一些重要产品放手给团队中的骨干/TL,让他们去规划、去驱动、去变革。我更多做了辅导性、资源性、业务判断的全局性工作。

随着时间的推移,越来越多的结果开始凸现:

  1. 斑马逐渐成为集团级的通用平台,数万小二同学用它发布管理业务。
  2. 方舟作为业务平台交付成为大促体系的核心部分,并成功应用到双11期间,在效率、效果和交付质量上取得瞩目的结果。
  3. 天马通过“开放共建、出海上云”,为集团多个BU提供基础的技术搭建服务。
  4. 在业务中,团队中沉淀出一个个技术产品:渲染引擎、API聚合网关等,也把曾经仅服务于内部的产品开放到商家、ISV。

在2018年中,提名参加晋升面试,结果未通过,技术委员会的评委们建议更多参与“集团级的技术建设、更高格局的视野”。

我很快走出了沮丧,毕竟在阿里,常胜不常有失败倒常伴。更重要是,主管 @四虎 的开导让我开始思考更全局的未来。

随后的时间,我继续投入了2018的双11前端整体工作,推进了多个端技术方案落地天猫。感谢阿里前端技术委员会主席圆心的举荐,我加入前端委员会,担任搭建技术方向的Sponsor - 旨在站在集团视角,定制标准、融合并打通搭建技术体系,在更大范围内赋能业务。

在集团内各个前端Leader的支持下,我们很快跨多个事业群达成了共识,制定完善了技术标准,基于一体化方案启动了多个层面的项目。

2019年5月,参加了年度晋升,顺利通过P9晋升。感恩。

P9:不止于前端

现在,仍然有太多的事情值得去深入:

  1. 以双11为代表的大促前端体系仍然是业内最具技术挑战的业务场景之一,包括不限于客户端容器技术、服务端渲染(Node)、框架与组件体系、跨终端技术等综合应用。
  2. 阿里有数以亿计的消费者,如何为我们的消费者构建一个好玩有趣的互动购物体验,是我们这个团队一直需要探索和改进的。
  3. 在未来,源自于前端的搭建技术,不仅能支持小二还能服务生态角色,不仅能支持国内还要服务全球。

除以上,我也深度参与阿里前端技术委员会的工作:

在阿里的前端体系,除了搭建方向以外,有相当数量的跨事业群共同建设的技术方向&项目,包括不限于Serverless、IDE、智能化、中后台、数据可视化、工程化、Node技术等。这些技术方向或者始于前端,但又不止于前端,共同为打造行业领先的技术生态服务。

有期待有更多的优秀的同学加入阿里,让我们的智力和努力触碰到数以十亿计的用户。欢迎加入联系我:wenliang.shuwl [at]alibaba-inc.com

最后分享一些过往些年的的心得:

关于工作:

1.想做好一件事情前,获得上级的认可非常非常重要,这是把一件事情做成、做好的催化剂。

2.很多时候,源自“事”的困难都可以用态度解决,来自“人”的困难可以用换位思考解决。

3.对于刚参加工作的同学,如果家中无矿,啥也别说,努力就对了!毕业五年内的表现,可以决定人生很多事情。

4.如果你和领导发生分歧:拿出你的数据/理由/态度尽最大的努力去说服他,如果充分沟通还形不成共识,那就先听他的 。这不是媚上,而是“在必须达成一致的前提下,相信更有概率做出正确决定的人。”

5.在工作中,如果某个时间突然发现和往常不一样,陡然压力变大/身心疲惫,别害怕 ,因为,这有可能是你在成长、突破瓶颈前的黑暗期。

6.专业技能是立身之本。从长期来看,它是性价比最高的投资标的之一。

7.结合资源,尽最大的努力、用最好的态度,做好手上的工作,也是一种创业。

8.不要因为背靠大树久了,就误认为自己是颗大树。对客户&用户的尊重和价值创造是我们很大的护城河。

关于生活:

1.技术人读书工作挣钱养家,一步步成长同时也会一岁岁变老。相比不少行业,互联网行业自身有赛道上的优势,但如果单纯以同比其他行业略高的溢价按年/月出售自己的时间和技能,这不应该是我们做的。坚持不断的思考、通过技术优势带来叠加价值,在过程中不断成长 , 这兴许会更好。

2.学会奖励自己。如果经受了辛劳、痛苦和压力走了过来,再不好好的、肉痛地奖励自己一把,哪对得起过去和未来的自己。

3.除了在工作中,人生处处皆可学习:学理财、学打球、学拍照、学游泳、学健身、学拍短视频、学习怎么把生活过得更好。

4.最后,咱们技术人可能很忙,有可能没法“work-life balance”,这是一种取舍。但健康的身体一定是最宝贵的一笔财富,没有之一,不能舍。谢谢大家。

作者:舒文

本文为云栖社区原创内容,未经允许不得转载。