前端开发的几点经验分享

13 篇文章 0 订阅
7 篇文章 0 订阅

前端开发的几点经验分享

前言

码农日益增长的需求功能需要同落后的代码实现之间的矛盾。

随着软件项目迭代,代码的日积月累,维护成本变得越来越高。

同样是实现功能,怎么能写出更有价值的代码,应该是码农要追求的道路。

本文由一个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只能算是抛砖引玉,真实的业务场景往往会更复杂。

复杂的场景建议使用数据驱动

但是也并不能说视图驱动就是错误的想法,当业务极其简单的时候,引入数据驱动会带来额外的成本,就像你公司就在你家对面,走路就可以了,你还要开车。

适合场景的才是最好的。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值