惯例在写正文之前扯一堆没用的,想直接看正文的话可以点 这里。
敢于自爆黑历史也是一种勇气。
刚才无意间看到了 AmazeUI。没忍住于是发了条动态:
现在再看 AmazeUI 感觉已经不是那么回事了,现在的几大前端框架以及各种工具改变了前端开发的几乎全部内容,除了基础语法以外都得重新学。。明明才过去几年啊。。想当年我还在纠结于写一个 CSS 库(类似于 <input> 等标签的样式库),某人问我:“你听说过可以点击链接不跳转页面,直接局部刷新的东西没?”我还觉得很不可思议,一个月后的寒假,我在家里一边看 Angular 一边写自己的用于局部刷新的 JS 库,同年暑假他说:“你们还在玩 Angular,整个前端界的目光都转向 React 了。”一年之后看中了 Vue,一直用到现在,一点造轮子的想法都没有了。。。。
作为一个前端界的萌新,我只有不到四年的开发经验,而且是伴随了我的大学生活,还混杂了后端、运维、安全等各种东西,所以经验并不如从业四年的大佬们。其实我现在更偏前端,纯属一个偶然,因为在我大一刚接触 Web 开发、还没把它当职业、没怎么认真学、前后端都只会一小点的时候,跟别人合作写一个项目,后端已经有人了,于是我就来写前端了。
当时我都会些啥?好像除了用 PHP 搞出个页面、用 jQuery 控制一下各种元素、添加一点动画(事实上动画那一块我当时也并不了解,只是照着手册写)以外,就没啥了。Ajax 之类的东西更是没听说过,还是后来才了解到的。那个项目对我前端的启蒙作用还是挺大的,至少我的 CSS 掌握的非常熟练了。
毕竟启蒙的时候没有大神来带,也不知道那些技术公众号,所以只能自己瞎折腾。记得当时看到过一句“不推荐用 HTML 标签和属性来控制样式”,于是我从那时候开始再也没写过 style
、bgcolor
等属性以及 <center>
等标签,大二九月份开始给下一届培训的时候也一直在强调,style
不推荐,剩下的这堆都已经过期了,千万不能用。
想想那个时候的好处和坏处都挺多的,好处有:要学的概念少、不用搭建复杂的开发环境、原理简单;当然坏处挺多的:要考虑浏览器兼容(依稀记得如何让 inline-block
和 border-box
兼容 IE 7)、很多复杂的效果无法实现、流量很贵。
当时我用的 3G 手机,每个月只有 200MB 流量,所以基本不敢用流量看网页,而且考虑到我写的网页可能被别人用流量访问,因此想尽各种办法节约空间:首先弃用 jQuery,因为我只用到了选择器、动画、Ajax,但是要加载好几十 KB 的库,不值得;其次写代码的时候我也尽可能压缩空间,例如 if (a === 1) func(x, y)
硬是被我写成了 if(a===1)func(x,y)
,.a > .b { display: inline; float: left; }
也被写成了 .a>.b{display:inline;float:left}
……节约变量名这种事情我也干出来过。后来发现了在线代码压缩工具之后高兴得不得了,每次写完之后必须要压缩一下,文件名是 xxx.min.js
,然后在同一目录下保留原始的 xxx.js
。
于是我开始写自己的样式库 RSCSS,只有一个文件,原则上尽可能精简,样式与 AmazeUI 比较像,以独立文件的形式对 IE 9 和 IE 7 做兼容,这样 Chrome 等浏览器就不用加载那么多 CSS 了。
后来的两件事改变了我,一件是 2014 年 10 月 HTML5 标准正式制定完成,我借机调查了一下身边小伙伴们使用的浏览器,大部分是现代浏览器,这意味着我可以在面向普通人的网站中使用一些新特性而只需要考虑有限的兼容;另一项是 4G 网络终于开始普及了,只要网页体积不大到丧心病狂的程度,一般人就是可以接受的,我也得以改变我的编码习惯,从“节约空间”到“提升可读性”转变。
// 现在微软强制升级 Windows 10 对前端开发者来说就是个福音,因为这标志着绝大多数网站不用兼容 IE 9 了。
2015 年寒假之前听说了一个可以点击链接之后不跳转页面、只进行局部刷新的框架(Angular 1),我试着照着 Demo 来写,发现好麻烦啊,网上的教学视频也把我看睡着了。不然我自己写一个吧!正逢寒假我们要写一个新系统,不然就边写库边写系统好了,这个库也可以用到系统中。于是我开始写 RSJS——一个封装了 Ajax 等常见操作、通过 location.hash
来做路由跳转、支持模板渲染、伪双向数据绑定(为什么是“伪”,下面就知道了)等功能的库。
同年暑假去七牛实习,现学了 NPM 和 Jekyll 这些现在非常常见的东西,然后不可避免接触了 Angular。但是由于当时水平还不够,也没有看英文文档的经验,所以还是折腾了一阵子(记得很清楚的是,ng-href
和 ui-sref
让我纠结了好久),顺便恶补了一下自己的学习能力,然后发现,之前自己摸索的那堆东西都是什么鬼。React 也看过,但是由于各种奇怪的问题(也因为能力不够吧),发现打包之后的一个 Hello world 居然有 4MB,于是果断放弃了。
半年后有幸去了 CODING,接触 Angular 的机会更多了,大部分知识都是在那儿学到的,我的学习能力有了很大的提升。后来也跟着公司转了 React。可以这么说:我目前对前端了解的知识的一半,都是那个时期学到的。以及做新项目的时候也接触了 ES6 和模块化,那叫一个方便。
大四开始对之前写的一个使用原生 JavaScript 和 PHP 的项目进行第三次重构(重构的原因是弹个框居然需要 innerHTML
,动态输出个表格居然需要 innerHTML
,生成一个表单居然需要 innerHTML
,修改输入检查的提示文字居然需要 innerHTML
……总之啥都需要 innerHTML
。于是我开始寻找拥有 Angular 一样的数据绑定功能、学起来非常容易、同时也非常精简的框架。一番寻找之后我发现了 Vue 1(之前我关注的所有技术号居然没有一个提到它),于是花了一个晚上边看手册边写,居然写完了好几个页面。后来大四的毕设我用了 Vue 2,同时对 Webpack 了解也更深了。
// 重构了那么多次的感想是:就算你一开始代码写的再烂,经过多次重构之后,即使还是用原生实现,不用现成的框架,最终你的代码也会变得跟使用了流行的框架一个样。所以发明了这些框架的人并不是天才,只是擅长在许许多多的项目经验中总结,并坚持用更好的方法来实现。包括各种设计模式也一样,这些东西的出现都是很自然的,如果一个人学这些东西觉得很麻烦,那可能是项目经验不多,或者做了许多初级项目,没有遇到必须通过这些方式解决的问题。
好了开始说正文。先贴一张图让大家看看:当时为了节约空间,我是怎么牺牲代码可读性的。
嗯就酱。这种风格大家看看、笑一笑就好了,下面如果出现了代码,那都是格式化过的。这两个项目都是我入门的时候写的,所以跟现在的思想有很大差别,代码水平也不怎么高,所以这两点就不要吐槽啦……
RSCSS 篇
跟当时的大多数框架(例如 AmazeUI)一样,一开始先嵌了一个 normalize.css
,然后是一段注释:
/**
* RSCSS by Rex Zeng
* 配色方案参考Flat-UI
* 网格布局参考Bootstrap
* 表单、表格样式参考Purecss
*/
基本样式
真的只是基本样式:
- 取消了所有元素的
-webkit-tap-highlight-color
和outline
- 美化了 Webkit 的滚动条,让 IE 的滚动条不占用空间(
@-ms-viewport
) - 修改 Webkit 自动补全的颜色为白色
- 调整了一些表单元素的垂直居中
- 规定了常见元素的字体、边距、颜色等样式
浮动提示框 rs-popup
当时 Material Design 还没出来,于是我写的尽可能平,顺便使用了 .rs-popup-fadein
、rs-popup-show
、.rs-popup-fadeout
来控制淡入淡出。
样式
左对齐 .left
、右对齐 .right
、左浮动 .fl
、右浮动 .fr
、字体颜色蓝色 .blue
、背景颜色蓝色 .bgblue
……受当时忘了哪儿的影响,将样式拆成了超级多的小 class,所以当时写出来的 HTML 有很多这样的:<div class="pointer fl red center ilb">鼠标指针为小手的、左浮动的、文字红色居中的行块级元素</div>
。
标题和内容
一些奇怪的 class:.rs-header
、.rs-container
、.rs-nav
、.rs-footer
、.rs-form
、.rs-table
……
按钮
对于按钮的颜色我就不再纠结于 blue
这样的单词了,模仿 BootStrap 改成了 primary
。
网格布局
模仿 BootStrap 的十二等分,自己实现了一个有点挫的。记得有一次面试,面试官问我“你对 BootStrap 的负 margin 有什么看法”,我当时根本答不上来,因为并没有研究的那么深,只是自己用 float
和 width
乱搞的一通而已。
标签
这里的 .rs-tabs
并不是标签页,而更像是 Tags 这种标签。比较好实现,就不多说了。
文件预览窗口(大窗口)
由于当时刚好要用 FlexPaper 来预览文件,因此就写了个嵌套 FlexPaper 的大窗口,带有关闭按钮。
差不多就是这些了。对于当时的我来说,能满足大部分需求。一个纯 CSS 文件,没有用任何打包工具和预处理器。
RSJS 篇
CSS 没有太多可说的,JavaScript 倒是有很多。
一些约定俗成的东西
- 首先先用
(function(window) { ... })(window)
来包裹代码避免变量扩散到全局 - 使用
use strict
减少语法灵活带来的问题 - 由于我受 jQuery 影响比较大,因此 RSJS 的标识符是
$
,一开始先var $
,然后是$.xxx = function (xxx) { ... }
,最后用window.$ = $
将其暴露至全局
对原生 JS 的扩充
由于当时 ES6 还没出来,更没有那些编译工具,还需要兼容一些旧浏览器,因此自己实现了 Array.prototype
的 map
、each
之类的函数,顺便还写了 unique
,也把这些方法添加到了 NodeList
中。实现的方法现在看起来都很简单,不说了。
选择器
我开始写这个库的时候,发现 IE 8 已经支持 querySelector
了,因此直接用了:
$ = function (selector, context) { return (context || document).querySelector(selector); };
$$ = function (selector, context) { return (context || document).querySelectorAll(selector); };
其实这一段是抄的别人的代码……
Ajax
使用 XmlHttpRequest 封装的(当时还没听过 Fetch API),强制 Content-type
为 application/json
,对常见的状态码做了错误提示,强制五秒超时。
DOM 操作
都是些 setHTML
、addClass
之类的简单函数,唯一比较特殊的是,我写了一个 replaceByHTML
的函数,可以将当前的 DOM 直接用 HTML 所产生的 DOM 替换。唯一的问题是,如果 HTML 字符串有多个根元素,那么只会用第一个来替换。
$.replaceByHTML = function (dom, html) {
dom.innerHTML = html;
var d = dom.childNodes[0].cloneNode(true);
dom.parentNode.replaceChild(d, dom);
};
getValue
、setValue
封装了常见表单元素获取值/设置值的方法,例如 <input type="text">
可以直接获取/设置 value
,但是 <input type="radio">
就不行。
事件监听
简单地使用了 addEventListener
来实现:
$.on = function (dom, type, func) { dom.addEventListener(type, func, false); };
$.off = function (dom, type, func) { dom.removeEventListener(type, func, false); };
检查输入合法性
这里的检查是针对 username
、repeat-password
、email
之类的检测,通过 rs-role
属性来标记这个输入框是什么类型。
伪·数据绑定
只针对表单,每个表单元素都可以加一个 rs-model
属性,例如 <input type="text" name="username" rs-model="login">
,这样只需要通过 $.getModel("login")
就可以获取所有的值(例如 { username: xxx }
)了。$.setModel("login", { username: xxx })
则可以将一个 JSON 对象按照 name
传给表单元素。
页面加载一开始先通过 $.loadModel()
将所有的 model 加载到 $.model
变量中,然后可以看一下 getModel
相关的代码,其它的可以脑补:
$.getModel = function (name) {
if (!$.model[name]) {
console.error("Model not found: " + name);
return null;
}
var o = {};
for (var i in $.model[name]) o[i] = $.getValue($.model[name][i]);
return o;
};
路由和跳转
感觉直接上代码比较好……
$.navigate = {
hash: window.location.hash.replace("#", ""),
get: []
};
$.jumpTo = function (route, popup) {
$.navigate.hash = window.location.hash = route;
if (popup) $.remove(popup);
};
这儿的 get
数组本来是用来做 Query String 分割的,忘了当时为啥没做了。
浮动提示框
其实就是屏幕最上面的 Toast。通过各种 setTimeout
来控制显示和隐藏。
$.popup = function (content, time) {
var id = parseInt(Math.random() * 100000),
wall = document.createElement("div");
wall.id = "rs-popup-" + id;
wall.className = "rs-popup rs-popup-fadein";
wall.innerHTML = content;
document.body.appendChild(wall);
wall.style.marginLeft = (-wall.offsetWidth / 2) + "px";
wall.style.marginTop = (-wall.offsetHeight / 2) + "px";
setTimeout(function () {
wall.className = "rs-popup rs-popup-show";
setTimeout(function () {
setTimeout(function () {
wall.className = "rs-popup rs-popup-fadeout";
setTimeout(function () {
$.remove(wall);
}, 500);
}, (time ? time : (wall.innerText.length * 80)));
}, 500);
}, 10);
return wall;
};
FlexPaper
调用 FlexPaper 的函数来生成一个文件预览窗口,没啥可说的。
打包工具
这个必须要提一下!当时并没听说过模块化,也没听说过 Grunt、Gulp 之类的东西,于是就自己写了一堆 .html
和一堆 .js
,每次通过 Ajax 加载并替换元素/执行代码,后来发现很恶心,于是自己用 Ruby 写了个拼接所有文件的东西。
#!/bin/env ruby
require "json"
class MvGenerator
attr_accessor :version
attr_accessor :appdata
def initialize(ver)
@version = ver
@appdata = {
:view => {},
:controller => {}
};
end
def add(filename, type)
(type == :view) \
? (lines = IO.readlines("view/" + filename + ".htm")) \
: (lines = IO.readlines("controller/" + filename + ".js"))
@appdata[type][filename] = lines.join.force_encoding("UTF-8").gsub(/(\n|\t)/, "").gsub("{{API_URL}}", "http://xxx.xxx.xxx.xxx:8000/apis/v1.0")
end
def output()
return JSON.dump(@appdata);
end
end
print "加入库文件...\n"
s = IO.readlines("rsjs.js").join.force_encoding("UTF-8").gsub(/(\n|\t)/, "").gsub("{{API_URL}}", "http://xxx.xxx.xxx.xxx:8000/apis/v1.0")
print "加入常量信息...\n"
s += "APPNAME=\"项目名称\";VERSION=\"#{ARGV[0]}\";"
gen = MvGenerator.new(ARGV[0])
print "加入控制器...\n"
Dir.glob("controller/*/*.js") do |file|
file = file.sub("controller/", "").sub(".js", "")
print "控制器 ", file, "\n"
gen.add(file, :controller)
end
print "加入视图...\n"
Dir.glob("view/*/*.htm") do |file|
file = file.sub("view/", "").sub(".htm", "")
print "视图 ", file, "\n"
gen.add(file, :view)
end
s += "APP=" + gen.output() + ";"
print "添加处理方法...\n"
s += IO.readlines("controller/index.js").join.force_encoding("UTF-8").gsub(/(\n|\t)/, "").gsub("{{API_URL}}", "http://xxx.xxx.xxx.xxx:8000/apis/v1.0")
print "生成最终文件...\n"
File.open("static-#{ARGV[0]}.js", "w") do |file|
file.write(s)
end
print "生成完毕!\n"
基本作用就是先把 RSJS 加进去,然后把所有的 .html
生成一个大 JSON 加到全局的 APP
变量中,然后把所有的 .js
加到后面,.js
文件本身通过 scope
的方式隔离开。这里的 scope
跟 Angular 中的完全不是一个概念,因为我当时并没有理解这是个啥玩意儿。不过运行的效果倒是挺不错的,截个图给大家看一下吧:
后续
后来这两个项目我就没再维护了,因为知道了更多的框架和工具,同时对自己的水平有了清晰的认识。之前一直以为“如果我当时水平也有那么高,说不定 RSJS 就可以知名了呢”,然而刚才我在翻 Vue 的 commit,发现它并不是一个新项目,早在我上大学之前 EvanYou 就开始开发了(在 这个提交 中,他把最一开始的 Element 改名为 Seed,当然后来又改成了 Vue。现在的 Element 是我比较喜欢的一套 Vue 组件库,不知道这是不是一种巧合),虽然当时只是个用于 “playing with data binding” 的个人项目,但是水平也比我写 RSJS 的时候好的太多。这个提交 的时间就是我刚开始写 RSJS 的时间,根本没有可比性。
然而并没什么可沮丧的。现在的水平比以前高了很多,也了解了很多新东西,并不需要通过造轮子来展现自己的水平(这么大的轮子一时半会儿也写不出来啊)。这应该也算是对自己水平的一个清晰的认知吧。能在跟上潮流的同时有自己的总结和思考,就已经是一个好目标了。