Coder Social home page Coder Social logo

faith's People

Contributors

fulvaz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

faith's Issues

前端小白爬坑记----一次失败外包的反思

这篇文章是对一次失败的外包项目的反思。

(周五v2上有个哥们问大家周末计划干嘛。嗯......我本来想说睡觉,然而最后写了篇长文。至于为啥拖了半年,你看文章长度就知道了,写文章真心累人而且纯粹是信仰)

背景

半年前非常非常缺钱花,因此接了个来自学院老师的外包项目,负责沟通的甲方是一位想转行的学校老师,另外还有投资人,偶尔露脸。而我们这边的主要负责人是我们的老师。和另一位同学商量了一下,直接实践前后端分离,后端以类似RESTful的风格设计api,我负责前端,工作内容是写与公众号绑定的Web App。然而第一次接外包,踩了很多坑,下面分章节说明。

需求不确定

学校项目往往没有文档。一般来说,这些项目在外面都是没人肯接,因为这些项目往往没有明确的需求,就算有也会常常变,稍微有点软件工程常识都知道这是大忌。当然,作为学生练手,也没啥关系,就是做得很累(就是9/12/7这样工作)。具体到这个项目中,文档是一个已经上线某app作为参考,甲方与负责人的口头说明,UI设计是自由发挥,技术选型也是自由发挥。

首先是沟通问题。其实需求变动不多,直接抄某app就可以,比较麻烦的是与app不同的内容,这种需求问题其实需要反复与甲方沟通、确认的,然而我这里采取的方法是快速出原型,做完一部分就给甲方看,然后沟通。这里最大的错误是我高估了自己的能力,对一个新手来说,写出健壮、高复用的代码实在太难,结果是,甲方要改的时候,我感觉很为难,修改常常意味着重写。其实在与甲方见面的时候应该认真把这部分给描述清楚,而不是三言两语。有一次功能都做完了,然后甲方提出了异议,表示与说好的不一样,后来发现是负责人与甲方意见不一,我们做的是负责人的要求。这种问题在会议上本可以沟通明白的。

UI设计中消耗了大量时间。让程序员自由发挥设计UI,叫程序员边写代码边做好PM实在是耍流氓。UI实在是一种见仁见智的东西,IOS7刚出来的时候也有人大骂库克毁了苹果,扁平化丑。所以是我的 git 分支里面有:indexV6、circleV3......事实上这些问题都应该避免的。厚着脸皮叫甲方搞定设计的问题,或者是自己找好人,让甲方付钱,一般这种情况,我推荐猪八戒随便找一个。不解决这个接下来会非常辛苦,比如说,甲方完全不懂移动app的设计规则,死活让你按照上个世纪的网页设计方法---把内容尽可能得塞进一个屏幕中。嗯,我觉得丑,甲方也这么觉得,然后让我改......我......我下次让甲方把UI找人弄好了再写前端。

技术选型

作为一个外包项目其实我是有私心的,希望将新技术用于实践中,然而这是非常鲁莽的,特别是在使用前没有提前进行详细的调查研究。

(不想引战,反驳请就事论事。我认为脱离场景讨论框架、语言、编辑器类似的信仰问题纯粹浪费时间。)

这个项目中使用vue是一个错误。在2016年12月时,vue2 刚出不久,整个生态还不完善,那时候很多UI库还只能用于1.x的版本中,插件也不完善,这对开发非常不利。

首先是UI库,我的标准是文档要完善,至少能快速上手,作者还在活跃地维护,满足这些标准的只有mint-ui和element-ui,但是这两个ui库还是不够好,幸好有源码,自己研究了下代码,再改了改总算满足甲方需求,但是拖了挺长时间。其次 vue 的生态问题,比如头像裁剪插件,可用的库只有 PC 版,需要使用类似的功能需要自己想办法将类似的插件整合进 vue 中,也花了不少力气。(事实上外包项目应该怎么简单怎么来,最好抄起jq就是干。甲方在项目后期苦恼地和我说找不到会 vue 的人。)然后就是因为不熟悉 vue 而踩到的许多坑,这无法避免。

当然在以后项目的技术选型我会保持慎重,不是新技术差,至少使用前要:1.对自己项目的需求有个详细的规划,需要什么功能,研究该框架的生态,什么插件需要自己写,什么功能有现成库,估摸新框架对项目的影响。2.选用前至少在其他小型项目使用过,对其中的坑有所了解,不要被新框架影响项目进度。比如那时候 vue 严格限制子组件不能直接将信息传递给父组件,设法绕过限制反而使代码更难以维护,后来改需求花了不少时间(现在已经重新加上双向绑定功能,不需要eventhub了)。

技术上的问题

技术上的问题远比人的问题简单。

mock server搭建

内容很简单,只是刚接触确实要推敲,还很麻烦。

项目中使用的是json-server,这个一个非常优秀的 mock 服务器,只需要写 json 就可以生成所需要的mock服务,但是json的问题是,不够灵活。折中的方法是写js文件,下文称这个文件为db.js,定义方法看json-server的文档。使用js文件设定mock服务的另一个好处是,这样可以将faker也集成进来,生成随机数据会非常简单。

但是这样也有问题。对需要传参的api,比如说/album/:uid, 那么我们需要模拟多个用户的相册,然而一下子弄出成百上千的用户数据是会严重拖慢json-server的速度的,而且我们也并不需要那么多的数据,我们只是希望通过不同的uid可以返回一个相册数据就可以了。那利用route就可以解决这个问题。

json-server中定义route的方法之一是写 json 文件,我们这里可以将不同uid的请求都指向同一个 api:

album/:id": "/album"

那么,我们只需要在db.js中定义一个album就可以访问任意 id 的相册。

另外,还有偷懒的方法:比如说有两个不同路径的api,但是他们返回的数据相同,或者是某些需要 api 方法是 POST,而且提交的数据并不需要在页面中使用,那完全可以在db.js中导出一个空数组,然后用路由将所需要的路径指向这个空数组的路径:

// db.js
module.exports = function() {
	data.album = {...}
	data.fakePost = []
	return data
}

// route.json
{
	"/postAct": "/fakePost"
}

这样就引出了另一个问题,一般来说,我们想 api 发送一个post请求,服务端通常会返回数据或者是响应码,而json-server只会把 post 请求中的数据发送回来。那么 mock 返回值就需要另一样东西:中间件。其实写一个json-server中间没多复杂,比如:

module.exports = function (req, res, next) {
  if (req.method === 'POST' && req._parsedUrl.pathname === '/userDetail') {
    req.method = 'GET'
  }
  next()
}

这段代码走的事情是,将所有对/userDetail路径的 post 请求拦截,改为 get 方法,然后运行下一个中间件。加了这样一个中间件之后,post /userDetail 请求就相当于 get /userDetail,因此,只需要在db.js中定义/userDetail的返回值,就可以实现 post 请求后返回响应码或者指定数据。

关于vue的坑坑洼洼

v-model

非常好用的一个功能,本质上是一个语法糖,vue的文档已经详细说明了原理,我在这里举一个例子。v-model应用范围不仅仅是在<input>中,比如说我们需要做一个弹出层的提示框,用户在提示框中输入文字,点击确定后,输入的内容要出现在页面上。我们将弹出层组件命名为popup,正常来说,我们在父组件中需要添加代码监控confirm事件,然后在popup中,点击确认按钮时触发confirm:

// index.vue
// template 
	<div>content in the popup: {{content}}
	<popup @confirm="onConfirm"></popup>
// script
	...
	methods: {
		onConfirm(payload) {
			this.content = payload
		}
	}
	
// popup.vue
// template
	<input v-model="text">
	<button @click="onClick">Confirm</button>
// script
	methods: {
		onClick() {
			this.$emit('confirm', this.text)
		}
	}

那如果使用v-model的话,那代码可以简化,应该说,使用这个组件的人会很舒服,因为现在只要需要向组件传入一个 data 就可以了:

// index.vue
// template 
	<div>content in the popup: {{content}}
	<popup v-model="content"></popup>
	
// popup.vue
// template
	<input v-model="inputText">
	<button @click="onClick">Confirm</button>
// script
	props: {
		value: String
	},
	data: {
		...{
			inputText: ''
		}
	}
	methods: {
		onClick() {
			this.$emit('input', this.inputText)
		}
	}

可以看出,在index中,使用popup组件变得非常简单,只需要将content传入v-model即可。

这里需要注意的是,popup中必须添加一个名为value的参数,父组件的 content 将会传到 value 中;另外popup中还添加了inputText的属性,原因是,vue不允许直接修改传入的props。完整代码,请点击

当然这个例子只是为了说明v-model可以减少代码量,事实上这个例子并不合理,有很多改进的地方。事实上组件确认后应该返回一个promise,让父组件随意处理输入的数据,这些就不展开说了,这又会是一篇文章。

操作DOM的问题

vue 虽然提供了数据绑定,但他也允许你直接修改dom,但是!自行修改 DOM 导致的风险 vue 可不管,你自己改了渲染结果,vue 不知道,因此不推荐修改DOM。

然而现实是残酷的,vue 不是万能,总有绕不过的坎,比如说,动画、移植插件等。下面是几条操作 DOM 的小规则。

在 vue 的生命周期里面,要到mount才能开始操作DOM,vue 提供了this.$el,通过这个就能使用原生的 js 代码操作已经生成的 DOM。

如果你所操作的 DOM 依赖 data 中的某些属性,当 data 变化时,你的 DOM是不会自动变化的,你需要在updated钩子函数中手动更新 DOM。

手动生成的 DOM 不会被组件内的 scoped css 影响,你需要自己在全局,或者是在 js 内写需要的样式。

为什么不允许直接修改props

'Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "ifLiked"

上文关于v-model的章节中我们提到vue不允许直接修改传入的props,vue 的警告信息已经清晰地说明了为什么,然而我们可以简单地通过新建一个data属性的方式绕过,然而绕过并没有解决问题。比如说,有这样一个场景:朋友圈点赞,然后设计如下:

  1. Card 表示一条朋友圈,其data中有一个属性为ifLiked,表示是否被当前用户点过赞
  2. 点赞按钮 Thumb,其props中有一个属性ifPink,用户点赞后,ifPink变为true,按钮变为粉红色。
  3. Card 是 Thumb 的父组件,加载时,Card 通过从服务端获取用户的点赞数据,然后将ifLiked传入 Thumb 的 ifPink中。

看出这样设计的问题了吗?

用户点赞时,Thumb 只修改了作为propsifPink!虽然 UI 看起来是正常的,然而父组件中的 ifLiked 却并不知道用户已经点了赞,因为 vue 禁止子组件直接修改父组件的属性,如果用户通过路由进入了其他页面,然后重新返回朋友圈会发现,自己点的赞全变成了灰色。

解决方法有三个:

  1. 通过事件监听将子组件的数据返回给父组件。然而这是一个糟糕的办法,原因很简单,如果你的票圈层级改为 Card-> Handlers -> Thumbs,Thumbs 并不是 Card 的直接父组件时,代码量急剧上升,因为 Handler 不仅要代理ifLiked,还要代理点赞事件!因为 Card 是无法直接监听 Thumbs 所发出的事件的。

  2. 发出点赞后,重新向服务器获取已点赞信息。

  3. 使用 eventhub 或者是 vuex ”修改“父组件内容(当然不是直接修改啦)。

不得不说,无论哪个方案都非常折磨人,于是:

在2017年4月底发布的 vue 2.3.0中,被删掉的.sync回归。日。

一定要进行错误处理

菜鸡代码的死穴是:脆弱。特别是移动端的前端,如果不做好错误处理,肯定要被老板和客户骂死。

程序逻辑的错误其实问题不大,大部分 vue 都给挡住了,大不了就是点击没反应,反正不至于白屏,这里重点说网络错误。

这个项目中的前端做成了SPA----大量 AJAX,那问题就是,如果有任意一个 AJAX 请求出了问题,但没有对用户的指令,这样的用户体验极差,特别是移动端网络不稳定。此外,出错没有任何提示信息也不利于修复 Bug,用户就两个字:白屏,这真是没办法。项目完结后,我总结了一下必须处理的错误分为两种:

  1. 页面加载时的发生的错误;
  2. 加载结束后网络请求的错误。

对第一种错误,发生时应该提示错误与错误码,然后自动刷新页面。在项目初期时,这样可以快速修复 Bug,错误码可以清晰地分辨出问题出自前端还是出自后端。在项目后期,这些提示就可以全部关掉了,项目已经成熟。如果这个项目可以继续的话,我是会将加载时的错误处理变为:

  1. 自动重新加载3次,然后提示网络错误,为用户提供手动重新加载的功能;
  2. 出现手动重新加载页面时,将错误信息、用户访问的页面、发生错误的api发送回服务器存log。

对第二种错误,自然提示网络错误,让用户重新点击,重新发送。当然了,如果用户发送的请求是发送已输入数据,发生错误的时候,将已输入数据保存到 LocalStroage 中,重新加载页面是将输入信息也提取出来。

最后要说明的是,在用户进行任意网络请求时,请加上反馈,比如进度条,比如提示加载成功。

至于在我的项目中,实施的方法很low,我简单带一下。首先,网络错误会在 Promise 中抛出,在调用 API 时处理 reject 就可以了,然后是自定义错误码的处理,后端的同学会将自定义错误以 JSON 的方式返回而且 HTTP 状态码统一返回 200 ,以防止运营商劫持。错误 JSON 格式如下:

{
	errcode: 5001,
	errmsg: ''
}

项目的 AJAX 通过vue-resource实施,而这个库带有interceptors,又是一个中间件,具体用法可以看文档,我定义了这么一个中间件:

Vue.http.interceptors.push((request, next) => {
  request.credentials = true
  next(response => {
    // 从response中获取数据
    let data = utils.response2Data(response)

    if (data.errcode && data.errcode !== 0) {
      response.status = data.errcode
      response.statusText = data.errmsg
      response.ok = false
    }
  })
})

这个中间件会将全部返回的请求都拦截检查一遍,如果发现数据中同时有errcodeerrmsg的字段,就自动将响应的statusstatusTextok替换,这样就能触发 Promise 的 reject 了,错误便可以在组件中统一处理。

关于 API 设计

因为缺乏文档的关系,在 API 设计上我与后端一起设计,首先说明我写一个组件的工作流程:

确定数据结构 - 设计 API - 写setting - 写文档 - 写api实现 - 组件设计 - 写页面

  • 确定数据结构:确定该组件所需要的数据,设计字段,以及该字段应该使用什么方式保存;

  • 设计 API:事实上我只是设计数据传输的字段,至于 API 的路径我不需要关心;

  • 写setting:由于前后端工作不同步、需求文档缺乏,API也只能边做边设计,这样就导致后端不知道需要提供什么字段,至于路径的设计更无从谈起。我提出的解决方法是,我与后端一起维护一个setting.js,其大致结构如下:

     var dev = true
     var apiPrefix = dev ? 'http://localhost:3000' : 'http://api.server.com'
     
     let devApis = {
       'circlesApi': apiPrefix + '/circles',
       ...
     }
     
     let apis = {
       'circlesApi': apiPrefix + '/circles', 
       ...
     }
     
     let api = dev ? devApis : apis
    

    这个setting.js中将开发的 mock API 与生产环境的 API 路径分离,这样我开时的 mock API 便的路径可以随便定义,写完后将setting.js的字段与定义告诉后端,后端在根据我提供的 API 行为文档自己去实现即可。需要说明的是由于setting.js是外部文件不用直接当做ES6模块导入,因此会存在缓存问题,即setting.js更新不及时,所以在请求时后 url 后加上一个随机的参数以强制刷新缓存,如url?timestamp=hash()

  • 组件设计:拆组件,哪些可以复用,如何高复用blablab,数据流blablabla

  • 写页面:写啊。

然后,我们两都是第一次搞前后端分离,在 API 设计上存在一些问题,经验总结如下:

  • 客户端知道尽量少的信息就可以使用 API,尽量精简 POST 请求中 JSON 数据的字段数,可以算的绝对不传。

  • 很多文章说应该使用 HTTP 方法作为一个动词,路径中充满名词,然而我们实践下来发现,HTTP 方法的动词要么不够用,要么不够直观,另外,不同人对 HTTP 方法的意义有不同的理解(比如PATCH与PUT),虽然翻文档可以解决问题,然而文档如果不更新(比如我们),API 就变的非常难以理解,到项目中期,我们将 API 的风格变为:

     	POST /moments => POST /moments/add
     	PATCH /moments/:id => POST /moments/:id/like
     	PATCH /moments/:id => POST /moments/:id/unlike
     	GET /moments 不变,不影响理解
     	POST /moments/:id/comment
     	POST /moments/:id/edit
    

    可以看出我们只使用 POST 与 GET 方法,路径的构成为名词 + 路径末尾动词,这样 API 所表达的意思直观得多。(这部分其实见仁见智,因为RESTful API设计没有统一的标准 update:其实已经完全放弃了REST)

  • 如果响应的某个字段为空,请使用一个默认值代替, 空字符串请用'',数组使用[],对象使用{},但绝对不要使用null或者undefined,前端如果没对你的数据进行检查,前端会直接蹦,而且过滤null非常烦人。因为前端会对数据进行处理,如调用字符串自带的方法、遍历数组、或者对象的值,对null或者undefined进行类似的操作会直接跑出错误,而 vue 中大部分情况会导致白屏,而且不能通过错误处理恢复。

CSS

  • 开发前要订好全局样式, 即标题文字大小, 描述文字大小, 颜色, 全局背景颜色,统一修改,不然散落各处那个酸爽;

  • 移动端慎重使用overflow-y:scroll, 手机滑动会卡顿,iphone也不例外;

  • 使用rem别用px,鬼知道甲方要不要你适配ipad。

其他

  • 用vue-cli的话,看看这个文档会让你少走许多弯路。这个文档详细说明了vue-cli的 webpack 模板设置与常见问题,包括静态文件处理、css处理器添加、环境变量、单元测试问题等;

  • yarn很棒,简单好用;

有空也可以去学学react和angular,这两位老大哥的社区里面关于设计模式的内容非常值得看;

  • mint-ui的源码很值得看,他们的设计非常棒,比如MessageBox的回调处理,看得我目瞪口呆,还有这种操作。有空再写写mint-ui源码分析;

  • 发包含id的数据考虑是否要先parseInt,可能后端只接受id: 1而不接受id: "1",说多都是泪;

  • this.$route和this.$router是两个完全不一样的东西;

  • 再说一次:如果后端发null和undefined给前端, 毫不犹豫地拿刀去见他。

PS

  1. 其实感觉甲方也挺惨,遇到我这种坑货,中间出了一堆坑全要耐着性子等我处理完。而且甲方人也很好,提前给尾款,过年还发红包,简直良心。但愿以后也能遇到良心老板。

  2. 哎,有耐性写这种文章的也就我这种有信仰的失业人员了。

  3. 这篇文章让我回想起了写硕士论文的恐惧,哎,网上分享干货与教程的大大真是伟大得不行,另外开源项目中负责写文档的哥们也是条汉子。

  4. 还没找到工作,求指点......

面试经历: 毕业一年腾讯前端社招初探

这次很印象深刻, 因为挺别扭的, 和我预期差很多, 要好好反思.

背景: 17年毕业211末流小硕, 某上市小公司工作一年多

1. 电话面

说明一下双向绑定原理.

简单, 手撸过. 就是面试官问到如何对div进行双向绑定有点蒙圈.
当然可以后面研究了一下, 发现可以用MutationObserver这个API, 有空我更新一下仓库的例子.

如果是在HTTP环境下, 页面被插了恶意代码, 有广告, 怎么办

这个问题很有意思, 那不外乎是如何识别, 还有识别到该怎么办.

识别可以在客户端算下html的hash, 然后服务端给个接口返回这个html的实际hash, 两个hash不一致就是被插了恶意代码.

如何去广告我的方案不是太好: 给所有合法的元素都加上id, 然后对DOM更新监听, 如果更新的DOM不带有这个id, 说明是广告, 直接去除.

现在回想一下, 可以考虑遍历script标签, 有没有不在白名单内的, 但也不是个好方案

混合应用开发, js与原生应用通信

没做过, 不知道

当然还有一些很基础的题目, 如XHR怎么用, HTTP的status code, method, 缓存相关的头, TCP, 首屏优化的方案, HTTP/2解决了什么问题. 这都很简单, 没什么好说的.

然后电话面过了.

一周后, 就是今天, 去现场面. 最近流感肆虐, 感觉自己快病死了. 起了个早, 打个的去了腾讯大厦.

2. 笔试

都是基础, 没什么好说的, 只是有几道题目电话面试也有.

就是有几道题目涉及code review有点惊喜, 这题目相当不错.

3. 一面

好巧, 就是电话面试那位.

又问了一次XHR怎么用

然后我表示在这之上的进行封装会更重要, 如实现promise, 设计中间件, 与方便的错误处理更加重要, 然后举了自己项目中的例子.

jsonp如何判断加载是否成功

还真不知道, 只知道onLoad和onError, 但是onLoad触发未必是成功. 思考了一番script的其他事件, 没啥头绪, 表示要去MDN找找.

找了一下, 要和readyState配合一下判断

上次后有没有了解过js和原生引用调用

行吧, 我是个喜欢总结失误的人.

简单说了一下最近准备的内容, 对jsbridge的理解, 还有最近的flutter等等.

然后指着笔试题上code review的问题问我什么原理

页面上有多个地方绑了window.onload, 然后是onload什么时候触发, 我记得不太清.

算法

把一个链表第p到第q个节点的顺序反转

就是链表反转升级版, 没啥好说的, 生病了脑子迷糊, 强行冷静下来后好好写就出来了.

就是用解构赋值用于交换, 面试官似乎不太理解. 行吧, 用传统方式写一个swap, 给面试官解释清楚了, 问题不大.

4. 二面

一看面容, 手表, 电脑, 还有发皱的工牌, 至少是老大级别了.

HTTPS加密流程说一下

握手, 非对称加密传密钥, 对称加密传数据. 到这里都还行, 依旧是基础.

然后开始看我的学校, 毕业时间, 哪工作

你在学校哪门课学得好

完, 要命题, 只要我没答出来我就可以走了. 然后我回是算法.

(其实选其他不会更好)

1~1000里面出现6的次数

答不出来, 其实这道题就在剑指offer里面, 不算太难. 然而答不出来要被怀疑专业素质, 基本没戏了

下面是尬聊, 好聚好散, 打道回府~

最后

就是挺郁闷的, 可以做好的事情没做好. 还行, 这周末休息, 玩游戏.

最近一次项目用到的优化技术

没图看这里:

http://fulvaz.leanote.com/post/%E6%9C%80%E8%BF%91%E4%B8%80%E6%AC%A1%E9%A1%B9%E7%9B%AE%E7%94%A8%E5%88%B0%E7%9A%84%E4%BC%98%E5%8C%96%E6%8A%80%E6%9C%AF-%E5%AF%B9%E5%A4%96

前言

虽然早就知道了这些优化方法, 但是能亲手用到实际项目中, 真是让人兴奋.

前面所说的"性能"如无特别交代, 都特指加载时间.

背景:

由于多方面原因, 某系统的前端的性能比较糟心, 加载速度和开发热更新速度比较慢, 不改的话开发新功能实在太辛苦了, 另外老板也会不开心.

结果:

首屏秒开, 热更新3s以内, 基本达到了重构的预期.

优化目标

用户开心 --- 加载速度要快, 至少瓶颈不能出现在前端
开发开心 --- 热更新要快, 发布要舒服, 添加新功能要舒服

性能优化

每个优化都不应该凭空捏造瓶颈, 应该有数据支持

立竿见影的方法

ng2 升级 ng5, 其实问题真的很少, 都是ng-template的问题, 改改api名字就可以了.

然后添加loadChildren

合并系统

某系统原先分为业务管理与客户管理, 用户交互是从官网首页进客户管理, 再选择对应客户, 跳转到业务系统, 经过调研后, 我对这两个系统进行了合并.

原因是:

  • 用户首次从客户管理系统跳转到业务系统时, js需要重新下载, 现在直接在缓存中加载js.
  • 开发和部署两套系统可不好玩
  • 代码可直接复用, 不再需要复制粘贴.

优化鉴权

原先的鉴权过程是:

下载全部资源文件 - 等待页面渲染 - 发送http请求 - 如果返回403 - 跳转到统一登录页 - 登录页渲染

其中等待页面渲染到登录页渲染完毕竟然需要超过6秒, 这对用户和开发者是非常不友好的. 找后端同学咨询了一下, 并没有公开的用来鉴权的接口, 只能访问数据接口, 然后判断返回结果.

我的做法很简单粗暴, 独立于angular项目编了一个js, 放到head中, 在等待页面渲染之前请求一个返回较快的接口, 如果403直接跳转到登录页, 不等angular渲染页面了.

有理有据地进行优化

优化到这里, 可以想到的都优化了, 虽然我觉得jquery相当碍眼, 但是去除jq的工作量太大, 贸然去掉的话会拖慢进度, 而且不能保证没有bug.

那么需要这个: webpack-bundle-analyzer

安装好后, 在package.json添加一个脚本

"build-stats": "ng build --stats-json && webpack-bundle-analyzer ./dist/stats.json"

然后

npm run build-stats

然后你就看到了这个(为了方便说明使用了很久以前的commit作为例子):

或者手动打开http://127.0.0.1:8888/

这张图详细地说明了每个模块的大小, 与模块的层级结构, 需要说明的是@angular与rxjs下的内容是完全不考虑优化的, 根本没优化空间, 观察了一番发现, zrender和echart引起了我们的注意 ---- 他们两很大, 加起来500kb左右, 而且在common.chunk中, 而首屏加载的js中就有common.chunk.

处理这两个巨无霸的方法有两个: 1. 懒加载, 当需要的时候再加载echart; 2.减少echart的大小

巧的是这里两个方法都能使用

懒加载

angular-cli项目其实自带code split, 使用import(path: string): Promise 详细看这里

这个和System.import可不一样, import是webpack的功能. 但是使用这个功能需要将tsconfig.app.json下的module的值设置为esnext

用法是: 在需要使用echart时, 比如某系统是在需要绘制时加载echart, 代码如下:

import('echart').then((echarts: any) => {
    // 做与echarts相关的事情
}

这样在打包是, echart会被独立打包为一个chunk.

减少大小

但是这样echarts还是太大了, 幸好echart提供了按需加载功能.

新建一个echart.lib.ts, 将自己项目所需要的echart组件导入进来, 比如某系统只使用了以下几个组件:

import 'echarts/src/chart/line';
import 'echarts/src/component/tooltip';
import 'echarts/src/component/title';
import 'echarts/src/component/legend';
import 'echarts/src/component/grid';
import 'echarts/src/component/toolbox';
import 'echarts/src/component/axis';
export * from 'echarts/src/echarts';

然后使用懒加载

// 这里指向echart.lib.ts的路径
import('./echart.lib').then((echarts: any) => {
    // 做与echarts相关的事情
}

一番优化后, 重新使用webpack-bundle-analyzer看下结果

ps: commit太多, 只能拿最后的最后结果凑数了.

可以看到优化后, echart在gzip后只剩311kb, 节约了200kb, echart只在使用时加载, 在加载过一次后, 后续能直接使用.

至于moment的优化: 套路是去掉不需要的locale, 方法比较简单粗暴, 可以去另一篇文章中看看.

架构上的优化

提取全局数据

观察发现某系统中每个组件都会依赖两个数据, 业务案件id与用户信息

业务案件id会根据路由变化而变化, 那么每个组件都需要在组件中获取一次业务案件id, 会这么写

this.router.events.subscribe(val => {
                if (val instanceof RoutesRecognized) {
                    this.eventId = val.state.root.firstChild.params.id;
                    
                    // 然后这里做网络请求, 以及后续依赖网络请求数据的操作
                }
                
            }, err => {

            });

这里引入了mobx做为全局状态管理工具
ps: 其实某系统没完全使用到mobx的功能, 只用了很小一部分

下面是引入mobx后获取业务案件id的方法

await this.eventStore.waitForInit();
this.eventId = this.advertiserStore.eventId;

而获取的具体实现则移到了mobx中, 实际上业务也不应该去关心如何取获取一个数据.

用户信息在旧系统中需要每个页面都获取一次, 而这个信息其实是变动很小的数据, 直接放全局中即可.

ps: 我认为业务组件应该只需要关心从哪可以获取数据就可以了, 如何获取数据应该是数据源该关心的事情.

用户切换工具

之前有点惨的一点是, 开发时每次调后端的api, 都需要跑去对应的页面拿token, 有时候手慢还拿不到, 后来12小时失效一次, 是在是可忍孰不可忍.

然后写了个工具, 有这两个功能

  • 将登陆集成到某系统中, 并提供了qa环境大多数账号的免密免验证码登陆, 调bug会舒服很多
  • 直接设置token功能, 也是能方便和测试沟通
  • 一键获取对应环境的token, 有时候登陆api失效, 但是qa或者测试环境已经登陆, 可以直接获取该环境的token

当然以上功能除了dev环境都会被全部移除, 基本告别复制token的开发方式.

网络模块优化

以前的网络模块将请求与页面耦合在了一次, 但是某系统有大量逻辑与网络请求状态变化有关, 如:

  • 是否具有投放广告的权限, 而这个仅能在数据获取api中可知
  • 内部错误码处理, 不同的错误码需要不一样的处理, 至少需要200行代码
  • loading, 需要由业务组件控制是否打开
  • 请求错误时, 上报错误到统计系统

如果这些全放到网络模块中, 这个模块不存在复用的可能性, 业务需要和底层逻辑完全解耦合, 而解除的方法是使用中间件.

受原来的网络模块启发, 原先的模块内可以添加一个中间件, 在网络请求请求前与响应后分别调用中间件的onResp与onReq函数.

在这基础上, 我多添加了两个钩子, onErr, onFin. 分别是错误发生时和请求结束(无论成功与否).

那处理loading就简单了, 写一个这样的中间件:

export class LoadingHttpMiddleware implements Interceptor {
    constructor(private loading: LoadingService) {}

    onReq(options) {
        if (!options.noLoading) {
            this.loading.show();
        }
    }

    onResp(options, res) {
    }

    onFin(options, res) {
        this.loading.hide();
    }

    onErr(options, res) {
    }

}

然后在app.component中添加这个中间件:

this.http.addInterceptor(new LoadingHttpMiddleware(this.loading));

这样, 一次网络请求的过程如下:


业务组件 reqData(interceptorOpts)

↓

网络模块 onReq(interceptorOpts)

↓

全部中间件 onReq(interceptorOpts)

↓

网络模块 onResp(interceptorOpts)

↓

全部中间件 onResp(interceptorOpts)

↓

网络模块 onFin(interceptorOpts)

↓

全部中间件 onFin(interceptorOpts)

↓

结束.

其中interceptorOpts则是中间件的选项, 如上面的loading中间件, 在发现options中发现有noLoading时, 则不显示loading, 至于这个字段叫什么由你来定义.

有兴趣可以看某系统的中间件代码, 在internal-http-middleware文件夹下

后记

真的要感谢同事的配合, 不然重构基本上是不可能成功的

你问为什么我可以不用写测试? 1. 业务绝对不动 2. 有qa啊

在Angular任意位置创建组件

在Angular任意位置创建组件

这篇文章主要说明了如何在路由以外的地方懒加载一个module, 然后手动生成组件, 然后渲染至页面上.

这篇文章的缘由是效果通重构后有这么一个需求: 需要将组件在modal框打开时懒加载js并动态生成组件, 那么传统的方法(设置entryComponent)便不可用了, 最后使用方法1, 配合项目内的modal框实现懒加载+动态生成

文中例子见(包括这次所使用的modal) https://github.com/fulvaz/ng-dynamic-component-exmple

modal的用法在例子的readme中

感谢这篇文章 真是写得棒极了,我在这基础上查了下资料,修正了几个问题,使其中的用法可以在ng5+AOT编译下可用。

下载回来装依赖, 然后npm run start就好了

方法1 SystemJsNgModuleLoader

ng5 aot可用, ng4大几率可以

s1. 在module中注入provider

{provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader},

s2. 组件内constructor插入依赖

private resolver: ComponentFactoryResolver,
private injector: Injector,
private loader: NgModuleFactoryLoader

s3. 加载模块

注意这里的路径是相对app.module,而不是相对使用这段代码的源文件。

loadModule() {
    this.loader.load('./a/a.module#AModule').then((factory) => {
      const module = factory.create(this.injector);
      const r = module.componentFactoryResolver;
      const cmpFactory = r.resolveComponentFactory(AComponent);

      // create a component and attach it to the view
      const componentRef = cmpFactory.create(this.injector);
      this.mainContainer.insert(componentRef.hostView);
    });
  }

需要说明的是原文说"所动态生成的组件需在module的entryComponent中声明", 实际上ng5是不需要的, ng4的同学需要在研究一下.

方法2 使用System.import懒加载


注意:AOT模式下无效

s1. 添加provider

注意: 使用@angular/platform-browser-dynamic"下的JitCompilerFactory, 因为@angular/compiler下导出的api被Angular团队视为私有api

Angular对"public api"的说明, 见https://github.com/angular/angular/blob/master/docs/PUBLIC_API.md

export function createCompiler(compilerFactory: CompilerFactory) {
  return compilerFactory.createCompiler();
}

{provide: COMPILER_OPTIONS, useValue: {}, multi: true},
{provide: CompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS]},
{provide: Compiler, useFactory: createCompiler, deps: [CompilerFactory]}

s2. 加载

System.import('./a/a.module').then((module) => {
      this.compiler.compileModuleAndAllComponentsAsync(module.AModule)
        .then((compiled) => {
          const m = compiled.ngModuleFactory.create(this.injector);
          const factory = compiled.componentFactories[0];
          const cmpRef = factory.create(this.injector, [], null, m);
          this.mainContainer.insert(cmpRef.hostView);
        })
    });

额外附赠

angular5的某个版本开始将compiler在@angular/platform-browser-dynamic"导出了,那么这就是可靠的公共api,用下面这种方法可以动态渲染模板

s1. 添加provider

export function createCompiler(compilerFactory: CompilerFactory) {
  return compilerFactory.createCompiler();
}

{provide: COMPILER_OPTIONS, useValue: {}, multi: true},
{provide: CompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS]},
{provide: Compiler, useFactory: createCompiler, deps: [CompilerFactory]}

s2. 渲染

renderTemplate() {
    const template = '<span>generated on the fly: {{name}}</span>';

    const tmpCmp = Component({template: template})(class {
    });
    const tmpModule = NgModule({declarations: [tmpCmp], entryComponents: [tmpCmp]})(class {
    });

    this.compiler.compileModuleAndAllComponentsAsync(tmpModule)
      .then((compiled) => {
        const factory = compiled.componentFactories[0];
        const compRef = this.container.createComponent(factory);
        compRef.instance.name = 123;
      });
  }

再次注意:使用@angular/platform-browser-dynamic"下的JitCompilerFactory

思考

为什么使用System.import在AOT模式下不行,但是NgModuleFactoryLoader却可以?Angular内部使用SystemJS进行模块加载,NgModuleFactoryLoader做了什么?

研究这个还是有意义的,可以写出api更简便的组件,特别是动态渲染模板,为实现匿名组件,高阶组件提供了土壤,甚至不再需要通过传入ng-template引用的方式自定义组件。

当前的开发效率还是偏低,大部分的锅是Angular的组件库质量实在相对低,其次---我的主观看法是,angular管理数据的方式相当原始,用得舒不舒服基本取决于个人水平。

[Angular5源码分析]SystemJsNgModuleLoader做了什么?

[Angular源码分析]SystemJsNgModuleLoader做了什么?

既然公司用了, 那也至少慢慢深入学习了

在AOT模式下, 无论是import或者Sytem.import得到的module都无法直接使用compiler编译得到结果, 为什么?

下面探究SystemJsNgModuleLoader源码

判断是否处于AOT模式

this._compiler instanceof Compiler

因为AOT模式下运行时没有compiler实例

在JIT模式下, 编译模板的代码如下

 private loadAndCompile(path: string): Promise<NgModuleFactory<any>> {
    let [module, exportName] = path.split(_SEPARATOR);
    if (exportName === undefined) {
      exportName = 'default';
    }

    return System.import(module)
        .then((module: any) => module[exportName])
        .then((type: any) => checkNotEmpty(type, module, exportName))
        .then((type: any) => this._compiler.compileModuleAsync(type));
  }

与我们的使用方法是一致的.

AOT模式下:

private loadFactory(path: string): Promise<NgModuleFactory<any>> {
    let [module, exportName] = path.split(_SEPARATOR);
    let factoryClassSuffix = FACTORY_CLASS_SUFFIX;
    if (exportName === undefined) {
      exportName = 'default';
      factoryClassSuffix = '';
    }

    return System.import(this._config.factoryPathPrefix + module + this._config.factoryPathSuffix)
        .then((module: any) => module[exportName + factoryClassSuffix])
        .then((factory: any) => checkNotEmpty(factory, module, exportName));
  }

可见二者使用System.import导入模块的路径是完全不同的

比如在读取./a/a.module#AModule时,

AOT下实际读取路径是: ./a/a.module.ngfactory
JIT: ./a/a.module

源码中有this._config.factoryPathPrefixthis._config.factoryPathSuffix这两个配置, 这两个属性控制了加载的实际路径

默认配置如下:

const DEFAULT_CONFIG: SystemJsNgModuleLoaderConfig = {
  factoryPathPrefix: '',
  factoryPathSuffix: '.ngfactory',
};

那基本可以下结论是绕过SystemJsNgModuleLoader懒加载module是黑科技, 因为import的路径可能会随着angular版本变化而变化..(而且也没法注入SystemJsNgModuleLoaderConfig)

然后另一件事情也很绝望

    return System.import(this._config.factoryPathPrefix + module + this._config.factoryPathSuffix)
        .then((module: any) => module[exportName + factoryClassSuffix])
        .then((factory: any) => checkNotEmpty(factory, module, exportName));

在读取到module后, 获取module的工厂的方式是module[exportName + factoryClassSuffix] ---- factoryClassSuffix也是个变量.

好吧, 总结一下, 如果我想作死绕过SystemJsNgModuleLoader懒加载module, 那实际的代码如下:

manualCompileAot() {
    System.import('./a/a.module.ngfactory').then((m) => {
      const factory = m['AModuleNgFactory'];
      const module = factory.create(this.injector);
      const r = module.componentFactoryResolver;
      const cmpFactory = r.resolveComponentFactory(AComponent);

      // create a component and attach it to the view
      const componentRef = cmpFactory.create(this.injector);
      this.mainContainer.insert(componentRef.hostView);
    });
  }

加载路径后添加了.ngfactory, 在获取module工厂时添加了NgFactory后缀

嗯, 我不作死.

结论

绕过SystemJsNgModuleLoader是作死

补充

  1. 这么说AOT模式下通过去掉Compiler包降低包大小, 在打包时编译全部模板
  2. 在需要使用相应模板时, 是通过获取工厂, 然后使用工厂创建相应的模板.

angular-cli下如何优化moment的大小?

angular-cli是无法使用ContextReplacementPlugin, 直接eject出来又感觉不妥.

之前重构项目研究了一下, 有以下几种办法

不需要locale

很简单, 使用moment-mini, 这是一良心人士去掉locale的版本.

需要使用locale

没有ContextReplacementPlugin只能使用很粗暴的方法

s1. 在package.json的scripts内添加这么一个命令

"postinstall": "node rm-moment-locale.js"

这条命令表示在每次安装依赖后都执行一次node rm-moment-locale.js

s2. 在项目根路径下新建一个名为rm-moment-locale.js的问题

var globby = require('globby');
var rimraf = require('rimraf');
const path = require('path');

globby([path.resolve(__dirname, '../node_modules/moment/locale/*'), `!${path.resolve(__dirname, '../node_modules/moment/locale/de-ch.js')}`,  `!${path.resolve(__dirname, '../node_modules/moment/locale/zh-cn.js')}`])
  .then(function then(paths) {
    paths.map(function map(item) {
      rimraf.sync(item);
    });
    console.log('remove redundent moment locales');
  });

代码中globby是正则匹配文件路径的工具, 比如上面的代码是保留简体中文和德语的locale.

s3. 重装一次依赖, 或者运行一次node rm-moment-locale.js

这样打包大小会下降不少.

但是作为一个有追求的程序员, 能就此止步吗? 怎么可能.

在扒了大神的源码后, 在ng-zorro中发现了端倪, [email protected]去掉了moment依赖, 转而采取其他方法实现与moment一样的功能.

优雅地优化moment大小(还在测试中)

TL;DR: 其实是用DatePipe和date-fns取代moment

参考: ng-zorro取代moment的方案

1. 格式化日期

使用DatePipe, 用法是在constructor中注入DatePipe, 然后调用其transform方法

transform方法接受两个输入(如需要时区与locale请参考angular文档)

this.datePipe.transform(date, format);

date: 可以是字符串, 也可以是一个Date对象
format: 所需要的日期格式, 详细见https://angular.io/api/common/DatePipe

当然也可以直接使用封装好的npm包:

npm install ng-time-format --save

用法见https://github.com/fulvaz/ng-time-format

2. 操作日期

使用date-fns替代momentjs, 他们两者功能相似

对比见:
date-fns/date-fns#275

好处是: 只要使用方法恰当, 打包时, 只会打包使用到的方法, 没使用的方法不会打包.

方法恰当是指需要导入指定的包路径, 而非引入整个包

// bad
import {addDays} from 'date-fns';

// good
import * as addDays from 'date-fns/add_days';

这样打包后, 包更小.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.