前端开发的几点经验分享
前言
码农日益增长的需求功能需要同落后的代码实现之间的矛盾。
随着软件项目迭代,代码的日积月累,维护成本变得越来越高。
同样是实现功能,怎么能写出更有价值的代码,应该是码农要追求的道路。
本文由一个demo入手,讲述下个人对这方面的理解。
一、DEMO
1.1 需求描述
实现一个简易浏览器,功能可以参考我们自己的浏览器。涉及功能如下
- URL输入框:可以输入URL
- 访问按钮:点击后访问这个页面
- 后退按钮:点击后退回到之前那个页面
- 前进按钮:点击后回到下一个页面(通常是点击后退后才能前进)
1.2 页面效果
后退 前进 访问
www.baidu.com
1.3 页面代码
<div>
<button id="prev">后退</button>
<button id="next">前进</button>
<input type="text" id="url" value="www.baidu.com"/>
<button id="search">访问</button>
</div>
<div id="page">
www.baidu.com
</div>
二、功能实现
2.1 视图驱动
事件触发
操作DOM
最直观的开发方式,所见即所得,也最简单。
优点:
- 难度:简单,符合平常人的思维方式,简单易上手
- 灵活:非常灵活,还有啥比直接操作DOM更灵活的吗?
缺点:
- 维护:难度简单到复杂递增,时间和需求增多,难度越大
- 排查:只能遍历代码块排查
代码实现:
var urlHistory = [];
var index = -1;
document.getElementById("search").onclick = function () {
var url = document.getElementById("url").value
urlHistory = urlHistory.slice(0,index+1)
urlHistory.push(url)
index ++;
document.getElementById("page").innerHTML = url;
}
document.getElementById("prev").onclick = function () {
if(index <= 0){
alert("到顶了")
return;
}
var url = urlHistory[-- index];
document.getElementById("page").innerHTML = url;
document.getElementById("url").value = url;
}
document.getElementById("next").onclick = function () {
if(index >= urlHistory.length - 1){
alert("到底了")
return;
}
var url = urlHistory[++ index];
document.getElementById("page").innerHTML = url;
document.getElementById("url").value = url;
}
2.2 数据驱动
数据与视图分开,适当的分工,职责更清晰。
就像流水线,哪个工作台做什么事情都是确定的。
事件触发
操作数据
映射器
操作DOM
是不是觉得似曾相识?没错React就是一个贯彻数据驱动思想的框架!
优点
- 维护: 难度一般,不变。每次修改只需修改对应的模块。
- 调试:容易,能快速定位错误发生的位置。
缺点
- 难度:一般,需要一定的设计,理解后难度降为简单
- 灵活:合理的设计能让它成为“灵活的胖子”
实现
// 视图渲染:专注DOM渲染,
function domRender(url){
document.getElementById("page").innerHTML = url;
document.getElementById("url").value = url;
}
// 映射器:数据与视图连接的地方,从数据对象中获取视图渲染需要的数据格式进行渲染
function renderUrlPage(urlManagementInstance) {
var url = urlManagementInstance.getData();
domRender(url)
}
// 数据管理:专注对数据的处理
function URLManagement (){
this.urlHistory = [];
this.index = -1;
}
URLManagement.prototype.getData = function () {
return this.urlHistory[this.index]
}
URLManagement.prototype.setData = function (url) {
this.urlHistory = this.urlHistory.slice(0,this.index+1)
this.urlHistory.push(url)
this.index ++;
return this;
}
URLManagement.prototype.setNext = function () {
if(this.index >= this.urlHistory.length - 1){
alert("到底了")
}else{
++ this.index
}
return this;
}
URLManagement.prototype.setPrev = function () {
if(this.index <= 0){
alert("到顶了")
}else{
-- this.index
}
return this
}
var urlManagement = new URLManagement();
// 事件触发
var URL_INPUT_DOM = document.getElementById("url")
document.getElementById("search").onclick = function () {
var url = URL_INPUT_DOM.value;
urlManagement.setData(url)
renderUrlPage(urlManagement);
}
document.getElementById("prev").onclick = function () {
urlManagement.setPrev()
renderUrlPage(urlManagement);
}
document.getElementById("next").onclick = function () {
urlManagement.setNext()
renderUrlPage(urlManagement);
}
三、锦上添花
3.1 代码健壮性:判断入参合法性
代码发生了非预期的错误,加入一些合法性判断和提示能知道具体错误在哪里。不然可能会出现一头雾水的报错,比如传入renderUrlPage
的参数错误了:
VM1297:1 Uncaught TypeError: urlManagementInstance.getData is not a function
错误到了浏览器,那只能进入具体代码去分析错误了。
正确的做法应该是:
// 映射器:数据与视图连接的地方,从数据对象中获取视图渲染需要的数据格式进行渲染
function renderUrlPage(urlManagementInstance) {
if(!(urlManagementInstance instanceof URLManagement)){
throw new Error("入参必须是 URLManagement对象!")
}
var url = urlManagementInstance.getData();
domRender(url)
}
3.2 命令模式
指令与实现分开,调用与执行解耦,暴露给外部的接口参数更少。
简而言之把使用者当成傻子,具体怎么调用方法我不管,我只需要知道指令,输入指令就可以执行对应的代码。
这里的应用场景是:我不需要知道怎么操作数据对象(URLManagement
)也不需要知道什么时候使用数据对象渲染数据渲染页面(renderUrlPage
)
var divRender = (function () {
// 视图渲染:专注DOM渲染,
function domRender(url){
document.getElementById("page").innerHTML = url;
document.getElementById("url").value = url;
}
// 映射器:数据与视图连接的地方,从数据对象中获取视图渲染需要的数据格式进行渲染
function renderUrlPage(urlManagementInstance) {
if(!(urlManagementInstance instanceof URLManagement)){
throw new Error("入参必须是 URLManagement对象!")
}
var url = urlManagementInstance.getData();
domRender(url)
}
// 数据管理:专注对数据的处理
function URLManagement (){
this.urlHistory = [];
this.index = -1;
}
URLManagement.prototype.getData = function () {
return this.urlHistory[this.index]
}
URLManagement.prototype.setData = function (url) {
this.urlHistory = this.urlHistory.slice(0,this.index+1)
this.urlHistory.push(url)
this.index ++;
return this;
}
URLManagement.prototype.setNext = function () {
if(this.index >= this.urlHistory.length - 1){
alert("到底了")
}else{
++ this.index
}
return this;
}
URLManagement.prototype.setPrev = function () {
if(this.index <= 0){
alert("到顶了")
}else{
-- this.index
}
return this
}
var urlManagement = new URLManagement();
// 命令
var COMMANDS = {
GO:"go",
NEXT:"next",
BACK:"back",
}
var OPERATOR ={
go:function (url) {
urlManagement.setData(url)
renderUrlPage(urlManagement);
},
next:function () {
urlManagement.setNext()
renderUrlPage(urlManagement);
},
back:function () {
urlManagement.setPrev()
renderUrlPage(urlManagement);
}
}
return {
COMMANDS:COMMANDS,
execute:function () {
var argus = Array.from(arguments);
if(argus.length === 0){
throw "指令错误,未输入指令"
}
if(!Object.values(COMMANDS).includes(argus[0])){
throw "指令错误,请从COMMONDS里面选择一个"
}
// 执行指令
OPERATOR[argus[0]](arguments[1])
}
}
})()
// 事件触发
var URL_INPUT_DOM = document.getElementById("url")
document.getElementById("search").onclick = function () {
var url = URL_INPUT_DOM.value;
divRender.execute(divRender.COMMANDS.GO,url)
}
document.getElementById("prev").onclick = function () {
divRender.execute(divRender.COMMANDS.BACK)
}
document.getElementById("next").onclick = function () {
divRender.execute(divRender.COMMANDS.NEXT)
}
上面还涉及到:闭包
、策略模式
、枚举
3.3 闭包
闭包最常用的功能就是模拟私有变量或者方法:
var module = (()=>{
var data = {}
function init(d) {data = d}
return function(){
console.log(data)
}
})()
将以上的代码用闭包的方式,能减少对全局变量的污染,对外只暴露module这个变量名。
3.4 策略模式
顾名思义,不同的“指令”对应不同的策略。如果有新的策略方案,则只需要在策略的集合里面加对应的key与策略,其他都不用操心。
在没有用策略模式的时候,你可能会这么写:
{
execute: function () {
var argus = Array.from(arguments);
if (argus.length === 0) {
throw "指令错误,未输入指令"
}
if (!Object.values(COMMANDS).includes(argus[0])) {
throw "指令错误,请从COMMONDS里面选择一个"
}
// 执行指令
if(argus[0] === 'go'){
urlManagement.setData(arguments[1])
renderUrlPage(urlManagement);
}else if (argus[0] === 'next'){
urlManagement.setNext()
renderUrlPage(urlManagement);
}else if (argus[0] === 'back'){
urlManagement.setPrev()
renderUrlPage(urlManagement);
}
}
}
又或者
{
execute: function () {
var argus = Array.from(arguments);
if (argus.length === 0) {
throw "指令错误,未输入指令"
}
if (!Object.values(COMMANDS).includes(argus[0])) {
throw "指令错误,请从COMMONDS里面选择一个"
}
// 执行指令
switch (argus[0]) {
case "go":
urlManagement.setData(arguments[1])
renderUrlPage(urlManagement);
break;
case "next":
urlManagement.setNext()
renderUrlPage(urlManagement);
case "back":
urlManagement.setPrev()
renderUrlPage(urlManagement);
}
}
}
如果使用策略模式将是以下情况:
var OPERATOR ={
go:function (url) {
urlManagement.setData(url)
renderUrlPage(urlManagement);
},
next:function () {
urlManagement.setNext()
renderUrlPage(urlManagement);
},
back:function () {
urlManagement.setPrev()
renderUrlPage(urlManagement);
}
}
OPERATOR[key]();
3.5 枚举
开发过程中经常会有魔法值
出现,除了当事人,其他人都不知道其含义,使用者必须翻代码才能知道这个意义。
所谓魔法数值,是指在代码中直接出现的数值,只有在这个数值记述的那部分代码中才能明确了解其含义。
解决魔法数值最好的办法就是常量,JS没有枚举,只能约定对象来实现。
这里的应用场景是:外部并不知道有 go、back、next这些指令,通过COMMANDS可以得知总得有三种指令,使用者更轻松输入正确的指令,而不必担心指令不存在或者写错。
// 这种方式也是可行的,只要你记得住
divRender.execute("back");
// 推荐方案
divRender.execute(divRender.COMMANDS.BACK);
四、总结
以上的DEMO只能算是抛砖引玉,真实的业务场景往往会更复杂。
复杂的场景建议使用数据驱动。
但是也并不能说视图驱动就是错误的想法,当业务极其简单的时候,引入数据驱动会带来额外的成本,就像你公司就在你家对面,走路就可以了,你还要开车。
适合场景的才是最好的。