Code Monkey home page Code Monkey logo

godbasin.github.io's Introduction

被删前端博客

【置顶】被删的前端游乐场

欢迎来被删的前端游乐场边学前端边撸猫噢~

如果你访问不了我的博客,可以来这里看哦~

最近尝试在 B 站做点技术相关的小视频~ id:被删
我的新书《前端的进击》上架啦!欢迎阅读背后的小故事~

最新

《前端性能优化--用户卡顿检测》
《让你的长任务在 50 毫秒内结束》
《前端性能优化--数据指标体系》
《有趣的 PerformanceObserver》
《前端性能优化--卡顿的监控和定位》
《从面试角度了解前端基础知识体系》
《复杂渲染引擎架构与设计--8.元素与事件》
《复杂渲染引擎架构与设计--7.离屏渲染》
《复杂渲染引擎架构与设计--6.增量渲染》
《复杂渲染引擎架构与设计--5.分片计算》
《复杂渲染引擎架构与设计--4.渲染计算》
《复杂渲染引擎架构与设计--3.底层渲染适配》
《大型前端项目的常见问题和解决方案》
《复杂渲染引擎架构与设计--2.插件的实现》
《复杂渲染引擎架构与设计--1.收集与渲染》
《如何进行前端职业规划》
《为什么项目复盘很重要》
《如何设计与管理一个前端项目》
《技术方案的调研和设计过程》
《前端性能优化--项目管理篇》
《前端性能优化--SSR篇》
《前端这几年--15.关于互联网寒冬》

复杂渲染引擎架构与设计

《复杂渲染引擎架构与设计--1.收集与渲染》
《复杂渲染引擎架构与设计--2.插件的实现》
《复杂渲染引擎架构与设计--3.底层渲染适配》
《复杂渲染引擎架构与设计--4.渲染计算》
《复杂渲染引擎架构与设计--5.分片计算》
《复杂渲染引擎架构与设计--6.增量渲染》
《复杂渲染引擎架构与设计--7.离屏渲染》
《复杂渲染引擎架构与设计--8.元素与事件》

前端技能提升

《如何进行前端职业规划》
《为什么项目复盘很重要》
《如何设计与管理一个前端项目》
《技术方案的调研和设计过程》
《大型前端项目的常见问题和解决方案》
《从面试角度了解前端基础知识体系》

前端性能优化

《前端性能优化--用户卡顿检测》
《让你的长任务在 50 毫秒内结束》
《前端性能优化--数据指标体系》
《有趣的 PerformanceObserver》
《前端性能优化--卡顿的监控和定位》
《前端性能优化--项目管理篇》
《前端性能优化--SSR篇》
《前端性能优化--容器篇》
《前端性能优化--Canvas篇》
《前端性能优化--卡顿篇》
《前端性能优化--渲染篇》
《前端性能优化--加载流程篇》
《前端性能优化--归纳篇》

工作杂谈

《前端这几年--1.转岗之路》
《前端这几年--2.工作原则和选择》
《前端这几年--3.关于成长和焦虑》
《前端这几年--4.生命与健康》
《前端这几年--5.沉淀习惯养成》
《前端这几年--6.工作选择的困惑》
《前端这几年--7.情绪与保持清醒》
《前端这几年--8.工作中的矛盾》
《前端这几年--9.提升工作效率》
《前端这几年--10.我的工作历险记》
《前端这几年--11.关于一年一换的魔咒》
《前端这几年--12.技术开发的门槛高吗》
《前端这几年--13.关于技术开发的职业发展》
《前端这几年--14.技术深度是伪命题吗》
《前端这几年--15.关于互联网寒冬》
《前端这几年--答辩晋级这件事》
《前端这几年--写文章这件事》
《写文章这件事》
《选择这件事》

前端杂谈

《我所理解的前端工程化》
《在线文档的网络层开发思考--职责驱动设计》
《如何设计一个任务管理器》
《在线Excel项目到底有多刺激》
《前端监控体系搭建》
《补齐Web前端性能分析的工具盲点》
《在线文档的网络层设计思考》
《VSCode 源码解读:IPC通信机制》
《VSCode 源码解读:事件系统设计》
《响应式编程在前端领域的应用》
《谈谈依赖和解耦》
《大型前端项目要怎么跟踪和分析函数调用链》
《多人协作如何进行冲突处理》
《前端构建大型应用》
《数据抽离与数据管理》
《组件配置化》
《一个组件的自我修养》
《页面区块化与应用组件化》
《前端模板引擎》
《对话抽象》
《前端思维转变--从事件驱动到数据驱动》

Angular框架解读

《Angular框架解读--预热篇》
《Angular框架解读--元数据和装饰器》
《Angular框架解读--视图抽象定义》
《Angular框架解读--Zone区域之zone.js》
《Angular框架解读--Zone区域之ngZone》
《Angular框架解读--模块化组织》
《Angular框架解读--依赖注入的基本概念》
《Angular框架解读--多级依赖注入设计》
《Angular框架解读--依赖注入的引导过程》
《Angular框架解读--Ivy编译器整体设计》
《Angular框架解读--Ivy编译器的视图数据和依赖解析》
《Angular框架解读--Ivy编译器之心智模型》
《Angular框架解读--Ivy编译器之AOT/JIT》
《Angular框架解读--Ivy编译器之增量DOM》
《Angular框架解读--Ivy编译器之变更检测》

深入理解Vue.js实战

前言 前端框架的出现
第一部分 Vue快速入门
第1章 Vue 框架介绍
第2章 Vue 环境快速搭建
第3章 Vue 基础介绍
第4章 Vue 组件的使用
第5章 常用指令和自定义指令
第6章 Vue 动画
第7章 Vue Router 路由搭建应用
第8章 实战:Todo List 从组件到应用
第二部分 Vue的正确使用方式
第9章 思维转变与大型项目管理
第10章 如何正确地进行抽象
第11章 全局数据管理与 Vuex
第12章 实战:三天开发一个管理端
第13章 实战:表单配置化实现
第14章 实战:使用 Webpack 或 Vue CLI 搭建多页应用
第15章 Vue 周边拓展
第16章 关于 Vue 3.0
后记 关于框架选型

前端面试

《前端面试这件事--1.面试准备》
《前端面试这件事--2.面试流程》
《前端面试这件事--3.专业知识概述》
《前端面试这件事--4.项目经验概述》
《前端面试这件事--5.其他内容概述》
《前端面试这件事--6.Javascript相关》

小程序应用

《小程序奇技淫巧之页面跳转管理》
《小程序奇技淫巧之日志能力》
《小程序奇技淫巧之globalDataBehavior管理全局状态》
《超实用小程序官方能力》
《小程序 gulp 简单构建》
《小程序的奇技淫巧之 watch 观察属性》
《小程序的奇技淫巧之 computed 计算属性》
《小程序上Typescript啦》
《小程序多页面接口数据缓存》
《小程序的登录与静默续期》

深入理解小程序

《小程序自定义组件知多少》
《理解小程序的安全与管控》
《解剖小程序的 setData》
《关于小程序的基础库》
《小程序页面管理与跳转》
《小程序的底层框架》
《小程序的诞生》
《认识小程序云开发》

全员学Vue

《9102全员学Vue--1.如何理解前端和Vue》
《9102全员学Vue--2.怎么三两下拼出一个页面》
《9102全员学Vue--3.把页面拼成个Web应用》

小程序开发月刊

《小程序开发月刊第一期(20190114)》
《小程序开发月刊第二期(20190215)》
《小程序开发月刊第三期(20190315)》
《小程序开发月刊第四期(20190415)》
《小程序开发月刊第五期(20190515)》
《小程序开发月刊第六期(20190617)》
《小程序开发月刊第七期(20190715)》
《小程序开发月刊第八期(20190815)》
《小程序开发月刊第九期(20190916)》
《小程序开发月刊第十期(20191015)》
《小程序开发月刊第11期(20191115)》
《小程序开发月刊第12期(20191216)》
《小程序开发月刊第13期(20200214)》
《小程序开发月刊第14期(20200314)》
《小程序开发月刊第15期(20200415)》

Typescript相关

《如何发布 typescript npm 包》
《关于Typescript》

一步一步走向应用开发

《SQL 与 NoSQL》
《认识数据库》

前端入门

《浏览器是如何进行页面渲染的》
《前端入门8--Ajax和http》
《前端入门7--代码调试》
《前端入门6--认识浏览器》
《前端入门5--Javascript》
《前端入门4--DOM和BOM》
《前端入门3--HTML和CSS》
《前端入门2--编写页面》
《前端入门1--什么是前端》

Vue2动画

《Vue2动画5--FLIP与列表过渡》
《Vue2动画4--多元素/组件过渡》
《Vue2动画3--Javascript钩子》
《Vue2动画2--CSS过渡与动画》
《Vue2动画1--transition组件》

D3.js-Tree实战笔记

《D3.js-Tree实战笔记8--曲线hover和点击》
《D3.js-Tree实战笔记7--曲线上的文字textPath》
《D3.js-Tree实战笔记6--添加右键菜单》
《D3.js-Tree实战笔记5--添加浮层》
《D3.js-Tree实战笔记4--添加拖动和缩放》
《D3.js-Tree实战笔记3--动态请求子节点》
《D3.js-Tree实战笔记2--简单的Tree demo》
《D3.js-Tree实战笔记1--拜见D3.js》

Vue2笔记

《Vue2使用笔记17--路由懒加载》
《Vue2使用笔记16--自定义指令》
《Vue2使用笔记15--自定义的表单组件》
《Vue2使用笔记14--Datetimepicker组件封装》
《Vue2使用笔记13--添加Promise弹窗》
《Vue2使用笔记12--使用vuex》
《Vue2使用笔记11--菜单鉴权》
《Vue2使用笔记10--路由鉴权》
《Vue2使用笔记9--监视路由》
《Vue2使用笔记8--vue与datatables(二):服务端渲染》
《Vue2使用笔记7--vue与datatables(一):浏览器渲染》
《Vue2使用笔记6--vue与各种插件和谐相处地创建表单》
《Vue2使用笔记5--transition过渡效果使用》
《Vue2使用笔记4--vue-router使用》
《Vue2使用笔记3--父子组件的通信》
《Vue2使用笔记2--创建左侧菜单栏Sidebar》
《Vue2使用笔记1--vue-cli+vue-router搭建应用》

纯前端的进军

《纯前端的进军6--套接字(Socket)与socket.io》
《纯前端的进军5--HTTP与Websocket》
《纯前端的进军4--网络进程通信和TCP/IP协议》
《纯前端的进军3--进程间通信》
《纯前端的进军2--多线程与IO》
《纯前端的进军1--线程与进程》

Cycle.js笔记

《Cycle.js学习笔记8--双向绑定Input值》
《Cycle.js学习笔记7--定位Input组件值获取bug》
《Cycle.js学习笔记6--合流》
《Cycle.js学习笔记5--关于框架设计和抽象》
《Cycle.js学习笔记4--使用Class和装饰器》
《Cycle.js学习笔记3--切换到Typescript》
《Cycle.js学习笔记2--使用cyclic-router来启动路由》
《Cycle.js学习笔记1--用Webpack启动应用》

webpack多页面配置

《webpack多页面配置8--静态资源loader们》
《webpack多页面配置7--source map和代码压缩》
《webpack多页面配置6--热加载刷新》
《webpack多页面配置5--开发服务启动》
《webpack多页面配置4--页面打包实现》
《webpack多页面配置3--打包相关node模块介绍》
《webpack多页面配置2--拿取页面目录名》
《webpack多页面配置1--基础webpack配置》

非科班恶补算法

《算法导论之js实现--n×n矩阵计算》
《算法导论之js实现--分治法求最大子数组》
《算法导论之js实现--快速选择》
《算法导论之js实现--堆排序》
《算法导论之js实现--快速排序》
《算法导论之js实现--归并排序》
《算法导论之js实现--插入排序》
《算法导论之js实现--计数排序》
《算法导论之js实现--冒泡排序》

Angular2-free

《玩转Angular2(13)--动态列表配置》
《玩转Angular2(12)--配置以及生成表单》
《玩转Angular2(11)--使用动态表单制作选项配置对话框》
《玩转Angular2(10)--向表单添加条件控制》
《玩转Angular2(9)--图片上传控件》
《玩转Angular2(8)--表单的radio和checkbox》
《玩转Angular2(7)--创建动态表单》
《玩转Angular2(6)--模型驱动和模板驱动的表单》
《玩转Angular2(5)--自定义input表单控件》
《玩转Angular2(4)--制作左侧自动定位菜单》
《玩转Angular2(3)--启用路由和添加静态资源》
《玩转Angular2(2)--改善应用配置》
《玩转Angular2(1)--用Webpack启动Angular2应用》

Angular2相关

《谈谈Angular--从Angular1到Angular4》
《谈谈Angular2的依赖注入》
《从Angular2-beta到Angular4-release框架升级总结》

Webpack相关

《正确的Webpack配置姿势,快速启动各式框架》

前端阶段性总结

《前端阶段性总结之「理解HTTP协议」》
《前端阶段性总结之「网络协议基础」》
《前端阶段性总结之「自动化和构建工具」》
《前端阶段性总结之「框架相关」》
《前端阶段性总结之「javascript新特性」》
《前端阶段性总结之「深入javascript」》
《前端阶段性总结之「掌握javascript」》
《前端阶段性总结之「总览整理」》

CSS相关

《图片居中新用法--巧妙使用background》
《CSS的display有关》
《CSS的position和z-index有关》
《js判断某个位置是否特定元素》

js相关

《async/await的使用》
《ES6/ES7好玩实用的特性介绍》
《将json输出为html(二):js数据类型判断实现》
《将json输出为html(一):字符串正则匹配》
《谈谈js的闭包》
《谈谈js的this》

jQuery相关

《jQuery插件--图片裁剪》
《jQuery插件--图片居中对齐》
《jQuery响应式瀑布流》
《做一个拖放功能的自定义页面》

three.js笔记

《three.js笔记5--添加鼠标移动视角》
《three.js笔记4--添加按键移动》
《three.js笔记3--添加光源》
《three.js笔记2--添加物体》
《three.js笔记1--初始化3D世界》

Angular1-free

《玩转Angular1(18)--使用mock本地数据模拟》
《玩转Angular1(17)--脚本自动更新并注册指令》
《玩转Angular1(16)--常用的angular方法》
《玩转Angular1(15)--指令们的相互协作》
《玩转Angular1(14)--使用$compile编译指令》
《玩转Angular1(13)--服务与指令的配合使用》
《玩转Angular1(12)--创建日期选择组件》
《玩转Angular1(11)--组件的自我修养》
《玩转Angular1(10)--使用Directive指令来添加事件监听》
《玩转Angular1(9)--按键事件队列KeyUp服务》
《玩转Angular1(8)--$q.all与async/await》
《玩转Angular1(7)--异步提交表单(文件)服务》
《玩转Angular1(6)--ui-router与嵌套路由》
《玩转Angular1(5)--$http服务封装为异常处理服务》
《玩转Angular1(4)--使用class写控制器》
《玩转Angular1(3)--在Angular中使用Typescript》
《玩转Angular1(2)--搭建angular项目》
《玩转Angular1(1)--webpack/babel环境配置》

BOX2DJS-tutorial

1. 基本概念
1.1 有关物理引擎
1.2 有关图像引擎
1.3 有关Box2D

2. 物理世界(world)

3. 形状(shape)和刚体(body)
3.1 形状
3.2 矩形
3.3 圆形
3.4 凸多边形
3.5 由形状到刚体

4. 关节(joint)
4.1 距离关节
4.2 旋转关节
4.3 移动关节
4.4 滑轮关节
4.5 齿轮关节

5. 操作(operation)
5.1 鼠标获取刚体
5.2 获取参与碰撞的刚体
5.3 获取刚体的各属性
5.4 为刚体设置属性
5.5 图形与刚体的结合

6. 创建一个物理世界吧
6.1 创建世界并初始化
6.2 添加边界
6.3 添加刚体
6.4 鼠标操作刚体
6.5 处理碰撞刚体

7. api
7.1 碰撞类(collision)
7.2 基础类(common)
7.3 动力学类(dynamics)

React-Redux笔记

《React-Redux使用笔记8--使用CSS过渡动画》
《React-Redux使用笔记7--嵌套路由的使用》
《React-Redux使用笔记6--创建Sidebar组件》
《React-Redux使用笔记5--创建Top组件》
《React-Redux使用笔记4--使用Redux管理状态》
《React-Redux使用笔记3--使用router完成简单登陆功能》
《React-Redux使用笔记2--完善打包生产代码流程》
《React-Redux使用笔记1--使用webpack搭建React开发环境》

Angular2笔记

《Angular2使用笔记9--使用Subject创建Websocket服务》
《Angular2使用笔记8--在Angular2中使用Observable对象》
《Angular2使用笔记7--Angular2中的基础概念》
《Angular2使用笔记6--使用服务类》
《Angular2使用笔记5--动画和制作index页面》
《Angular2使用笔记4--路由和导航》
《Angular2使用笔记3--创建头部组件》
《Angular2使用笔记2--创建登录页面》
《Angular2使用笔记1--搭建Angular2项目》

Vue笔记

《Vue使用笔记4--Vue事件、过渡和制作index页面》
《Vue使用笔记3--创建头部组件》
《Vue使用笔记2--vue-router与创建登录组件》
《Vue使用笔记1--使用vue-cli搭建Vue项目》

React笔记

《React使用笔记8--组件间的通信》
《React使用笔记7--关于Refs和React表单》
《React使用笔记6--使用flux"单向流动"你的应用》
《React使用笔记5--理解jsx以及制作index页面》
《React使用笔记4--创建头部组件》
《React使用笔记3--组件的State/Props与生命周期》
《React使用笔记2--创建登录组件》
《React使用笔记1--使用webpack搭建React项目》

Angular笔记

《Angular使用笔记15-在Angular中使用Echarts》
《Angular使用笔记14-在Angular中使用百度地图》
《Angular使用笔记13-对指令Directive进行单元测试》
《Angular使用笔记12-Karma的一些配置项》
《Angular使用笔记11-使用Karma和Jasmine进行单元测试》
《Angular使用笔记10-有关路由以及控制器间通信》
《Angular使用笔记9-使用sessionStorage判断是否已登录》
《Angular使用笔记8--使用filter服务进行格式转换》
《Angular使用笔记7--使用File API编写预览图片的指令》
《Angular使用笔记6--编写异步提交带图片的表单服务》
《Angular使用笔记5--作用域简单分析以及制作index页面》
《Angular使用笔记4--制作头部指令》
《Angular使用笔记3--公用信息的管理》
《Angular使用笔记2--创建登录页面》
《Angular使用笔记1--搭建Angular项目》

关于我

《被删的前端游乐场建成!》
《关于我》
《我的前端入门之路》
《前端在变,然而热情不变》

有个技术无关的微信公众号: 牧羊的猪

乞讨码

image

码生艰难,写文不易,给我家猪囤点猫粮了喵~

godbasin.github.io's People

Contributors

godbasin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

godbasin.github.io's Issues

正确的Webpack配置姿势,快速启动各式框架

本文介绍一些Webpack常用或者有意思的一些配置,教你快速启动各种框架(这里主要是React和Angular)。该篇我们不聊原理,只讲实战。

在去年的这个时候,本*年还在被Grunt和Gulp以及各种Requirejs、Seajs团团围住攻击,狼狈不堪。后面认识了Webpack之后,基本所有项目框架都拿它来构建了。
当然也不包括本*年负责项目都是纯前端的PC端单页应用的原因,还没遇到什么项目使用Webpack上太难的问题。


Hello Webpack

Webpack是一个现代的JavaScript应用程序的模块打包器(module bundler)。其实Webpack不应该拿来跟Grunt/Gulp比较的,但在本*年这边它就是承担起了很大一部分工作。

初始Webpack

这里主要基于Webpack2来讲吧,Webpack1迁移到2还是不是特别难的,官方也配了迁移文档
其实官方的文档也有很详细的说明了,对于一般的项目还是可以完全驾驭的。

下面我们先跟随着原始的脚步过一遍概念吧。

四个核心概念:入口(entry)、输出(output)、loader、插件(plugins)。

入口(entry)

将您应用程序的入口起点认为是根上下文(contextual root)或app第一个启动文件。
一般来说,在Angular中我们将是启动.bootstrap()的文件,在Vue中则是new Vue()的位置,在React中则是ReactDOM.render()或者是React.render()的启动文件。

module.exports = {
  entry: './path/to/my/entry/file.js'
};

同时,entry还可以是个数组,这个时候「文件路径(file path)数组」将创建“多个主入口(multi-main entry)”。

常见的使用方式是我们需要把"babel-polyfill.js"这样的文件也注入进去(如果需要React的话还可以加个"react-hot-loader/patch"进去):

module.exports = {
  entry: ['babel-polyfill', './path/to/my/entry/file.js']
};

出口(output)

output属性描述了如何处理归拢在一起的代码(bundled code),在哪里打包应用程序。一般需要以下两点:

  • filename: 编译文件的文件名(main.js/bundle.js/index.js等)
  • path:对应一个绝对路径,此路径是你希望一次性打包的目录
module.exports = {
  output: {
    filename: 'bundle.js',
    path: '/home/proj/public/assets'
  }
};

loader

webpack把每个文件(.css, .html, .scss, .jpg, etc.) 都作为模块处理。但webpack只理解JavaScript。

如果你看过生成的bundle.js代码就会发现,Webpack将所有的模块打包一起,每个模块添加标记id,通过这样一个id去获取所需模块的代码。
而我们的loader的作用,就是把不同的模块和文件转换为这样一个模块,打包进去。

loader支持链式传递。能够对资源使用流水线(pipeline)。loader链式地按照先后顺序进行编译,从后往前,最终需要返回javascript。

不同的应用场景需要不同的loader,这里我简单介绍几个(loader使用前都需要安装,请自行查找依赖安装):

1. babel-loader

官网在此,想要了解的也可以参考Babel 入门教程
babel-loader将ES6/ES7语法编译生成ES5,当然有些特性还是需要babel-polyfill支持的(Babel默认只转换新的JavaScript句法,而不转换新的API,如Promise等全局对象)。

// 在webpack1里使用loader属性,在webpack2中为rules属性
module.exports = {
  module: {
    rules: [
      {test: /\.(js)$/, use: 'babel-loader'}
    ]
  }
};

而对于babel-loader的配置,可以通过options进行,但一般更常使用.babelrc文件进行:

{
    "presets": [], 
    "plugins": [] // 插件
}
  • presets: 设定转码规则

有"es2015", "stage-0/1/2/3",如果你使用React则还加上"react",而我一般使用"lastest"装满最新特性。
当然这些都需要安装,你选择了对应的转码规则也要安装相应的依赖:

npm install --save-dev babel-preset-latest

2. ts-loader

一看就知道,是个typescript的loader,同样的还有awesome-typescript-loader,关于两者的不同可参考作者的话
这里我们使用ts-loader也就足够了:

{
    test: /\.(ts)$/,
    use: ["babel-loader", "ts-loader"],
    exclude: /node_modules/
}
  1. 其他loader
  • css相关loader

    • css-loader: 处理css文件中的url()
    • style-loader: 将css插入到页面的style标签
    • less-loader: less转换为css
    • postcss-loader(autoprefixer-loader): 自动添加兼容前缀(-webkit--moz-等)
  • url-loader/file-loader: 修改文件名,放在输出目录下,并返其对应的url

    • url-loader在当文件大小小于限制值时,它可以返回一个Data Url
  • html-loader/raw-loader: 把Html文件输出成字符串

    • html-loader默认处理html中的<img src="image.png">为require("./image.png"),需要在配置中指定image文件的加载器

插件(plugins)

loader仅在每个文件的基础上执行转换,插件目的在于解决loader无法实现的其他事
由于plugin可以携带参数/选项,需要在wepback配置中,向plugins属性传入new实例。

这里也介绍几个常用的插件:

1. HtmlwebpackPlugin

功能有下:

  • 为html文件中引入的外部资源如script、link动态添加每次compile后的hash,防止引用缓存的外部文件问题
  • 可以生成创建html入口文件,比如单页面可以生成一个html文件入口

但其实最常使用的,无非是把index.htnm页面插入(因为入口文件为js文件):

new HtmlwebpackPlugin({
    template: path.resolve(__dirname, 'src/index.html'),
    inject: 'body'
})

2. CommonsChunkPlugin

提取代码中的公共模块,然后将公共模块打包到一个独立的文件中,以便在其他的入口和模块中使用。

像之前有个项目有远程服务器调试代码的需求[捂脸],这时候我们需要把依赖单独抽离出来(不然文件太大了):

new CommonsChunkPlugin({
    name: 'vendors',
    filename: 'vendors.js',
    minChunks: function(module) {
        return isExternal(module);
    }
})

关于isExternal()函数,用了很简单的方式进行:

function isExternal(module) {
    var userRequest = module.userRequest;
    if (typeof userRequest !== 'string') {
        return false;
    }
    return userRequest.indexOf('node_modules') >= 0; // 是否位于node_modules里
}

3. webpack.ProvidePlugin

定义标识符,当遇到指定标识符的时候,自动加载模块。像我们常用的jQuery:

new webpack.ProvidePlugin({
    jQuery: 'jquery',
    $: 'jquery'
})

4. ExtractTextPlugin
可以将样式从js中抽出,生成单独的.css样式文件(同样因为方便调试[捂脸+1])。即把所以的css打包合并:

new ExtractTextPlugin('style.css', {
    allChunks: true // 提取所有的chunk(默认只提取initial chunk,而上面CommonsChunkPlugin已经把部分抽离了)
})

解析(resolve)

这些选项能设置模块如何被解析。这里本*年只讲两个常用的:

1. resolve.extensions

自动解析确定的扩展。默认值为:

resolve: {
    extensions: [".js", ".json"]
    // 当我们需要使用typescript的时候,需要修改:
    extensions: [".js", ".ts"]
    // 当我们需要使用React时,还要修改:
    extensions: ['.ts', '.tsx', '.js']
}

2. resolve.alias

创建importrequire的别名,来确保模块引入变得更简单。

resolve: {
    alias: {
        shared: path.resolve(__dirname, 'src/shared'),
    }
}

如果使用typescript的话,我们还需要配置tsconfig.json

{
    "compilerOptions": {
        "paths": {
            "shared": ["src/shared"]
        }
    }
}

这样我们就可以跟长长的路径定位说拜拜了:

// 原代码
import {something} from '../../../../../shared/something';
// 配置后
import {something} from 'shared/something';

开发工具(devtool)

此选项控制是否生成,以及如何生成source map。
要开启source map,我们还需要安装source-map-loader

npm i -D source-map-loader

同时添加loader的配置:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ["source-map-loader"],
        enforce: "pre"
      }
    ]
  },
  devtool: 'source-map' // 然后我们就可以开启了
};

webpack-dev-server

webpack-dev-server是webpack官方提供的一个小型Express服务器,主要提供两个功能:

  1. 为静态文件提供服务
  2. 自动刷新和热替换(HMR)

在实际开发中,webpack-dev-server可以实现以下需求:

  • 每次修改代码后,webpack可以自动重新打包
  • 浏览器可以响应代码变化并自动刷新

一般来说,我们可以通过引入webpack.config.js文件然后调整配置就可以启用了:

// webpackServer.config.js文件
// 生成配置
var webpack = require('webpack');
var path = require('path'); // 引入node的path库
var config = require("./webpack.config.js"); // 引入webpack.config.js
config.entry.concat(['webpack/hot/dev-server',
    'webpack-dev-server/client?http://localhost:3333'
]);
module.exports = config;

然后命令行启动:

webpack-dev-server --config webpackServer.config.js --host 0.0.0.0 --port 3333 --devtool eval --progress --colors --hot --content-base dist

看着有点长,其实webpack-dev-server --config webpackServer.config.js后面的都是配置,也可以在webpackServer.config.js文件中写入。

常用配置:

  • devServer.contentBase: 告诉服务器从哪里提供内容
  • devServer.port(CLI): 指定要监听请求的端口号
  • devServer.host(CLI): 指定使用一个host。默认是localhost
  • devServer.hot: 启用webpack的模块热替换特性
  • devServer.progress(CLI): 将运行进度输出到控制台。

其余配置请移步官方文档


前端框架与Webpack

这里本*年就不一个个讲解了,简单分享几个用过的webpack.config.js配置吧。

Angular1 + Webpack

var webpack = require('webpack');
var CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var path = require('path');
var config = {
    entry: {
        app: ['./app/bootstrap.js'] // 入口文件
    },
    output: {
        path: path.resolve(__dirname, 'app/entry'), // 编译后文件
        publicPath: '/entry/',
        filename: 'bundle.js' // 生成文件名
    },
    module: {
        loaders: [{
            test: /\.js$/,
            loader: 'babel-loader',
            exclude: /node_modules/
        }]
    },
    plugins: [
        // 使用CommonChunksLoader来提取公共模块
        new CommonsChunkPlugin({
            name: 'vendors',
            filename: 'vendors.js',
            minChunks: function (module) {
                var userRequest = module.userRequest;
                if (typeof userRequest !== 'string') {
                    return false;
                }
                return userRequest.indexOf('node_modules') >= 0
            }
        })
    ]
};
module.exports = config;

教程请移步玩转Angular1(1)--webpack/babel环境配置

React + Webpack

var webpack = require('webpack');
var path = require('path'); //引入node的path库
var HtmlwebpackPlugin = require('html-webpack-plugin');
var config = {
    entry: ['webpack/hot/dev-server',
        'webpack-dev-server/client?http://localhost:3000',
        path.resolve(__dirname, 'app/index.js')
    ], //入口文件
    output: {
        path: path.resolve(__dirname, 'dist'), // 指定编译后的代码位置为 dist/bundle.js
        filename: 'bundle.js'
    },
    module: {
        loaders: [
            // 为webpack指定loaders
            {
                test: /\.less$/,
                loaders: ['style', 'css', 'less'],
                include: path.resolve(__dirname, 'app')
            },
            {
                test: /\.jsx?$/,
                loader: 'babel-loader',
                exclude: 'node_modules'
            }
        ]
    },
    plugins: [
        // 入口模板文件解析
        new HtmlwebpackPlugin({
            title: 'React Redux Test',
            template: path.resolve(__dirname, 'templates/index.ejs'),
            inject: 'body'
        })
    ],
    devtool: 'source-map'
}
module.exports = config;

教程请移步React-Redux使用笔记1--使用webpack搭建React开发环境

Angular2 + Typescript + Webpack

var webpack = require('webpack');
var path = require('path');
var HtmlwebpackPlugin = require('html-webpack-plugin');

var config = {
    entry: ['babel-polyfill', './src/bootstrap.ts'],
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: './bundle.js'
    },
    resolve: {
        extensions: ['.ts', '.js']
    },
    module: {
        rules: [
            {
                test: /\.ts$/,
                use: ["babel-loader", "ts-loader", "angular2-template-loader"],
                exclude: /node_modules/
            },
            {
                test: /\.(html|css)$/,
                use: ['raw-loader'],
                exclude: [path.resolve(__dirname, 'src/index.html')]
            },
            {
                test: /\.async\.(html|css)$/,
                loaders: ['file?name=[name].[ext]']
            }, {
        test: /\.js$/,
        use: ["source-map-loader"],
        enforce: "pre"
      }]
    },
    plugins: [
        new HtmlwebpackPlugin({
            template: path.resolve(__dirname, 'src/index.html'),
            inject: 'body'
        }),
        new webpack.ContextReplacementPlugin(
            /angular(\\|\/)core(\\|\/)@angular/,
            path.resolve(__dirname, 'src'),
            {}
        )
    ],
    devtool: 'source-map'
};

module.exports = config;

教程请移步玩转Angular2(1)--用Webpack启动Angular2应用


结束语

很可惜,当时玩Vue本*年是用的vue-cli,所以这里没有Vue相关的代码。不过经过上面的讲解以及课后的练习,相信你一定可以搭建自己想要的应用。
Webpack的资源很多,而深入理解的你也能去开发自己想要的loader或是插件的,多了解多尝试总是很棒的。

页面区块化与应用组件化

很多时候,我们喜欢划分所谓技术需求和业务需求。但其实,技术来自于业务,业务需要技术支撑,两者是分不开的。我们可以尝试在业务里,一样地写出花来。

页面区块化

其实页面区块化只是一种瞎说的术语,个人想说的是,将一个页面清晰地按照功能、业务、emmmmmmm。。。可能就是按照功能划分吧。这其实不完全属于前端的范畴,是贯穿业务、产品、设计、前端、甚至后台、终端的一整个团队的设计吧。

什么是区块

我们来看看常用的应用吧,首先是知乎:

image

简单划分下:

image

大致我们可以分为三大块:

  • 头部:快速导航栏
  • 左侧:内容板块
  • 右侧:推广板块

其实论坛类、博客类的页面大多如此,看看微博:

image

除此之外,还有视频类、电商类等各种角色的网站,大家有空也可以去看看,思考下里面是怎么划分的。

或许你会觉得,想这些有什么用呢?这对我的工作有什么帮助吗?嘛,个人觉得观察 -> 思考 -> 总结也是有意思的事情,可以多一种角度来思考自己的工作,也能提高工作的趣味性吧。

如果你非要问,到底有什么用嘛~酱紫,容我细细道来吧。

应用组件化

说到组件,这下总该不陌生吧,翻译下就是 component,这每个框架里面不都一抓一大把嘛。

什么是组件

简答来说,组件可以扩展 HTML 元素,封装可重用的代码

<!--长这样-->
<my-component></my-component>

你说,啊???这什么都没有啊。停停停,这只是我们最后使用的样子,逻辑嘛,都封装在里面啦。

一个组件,它的呈现可能会千奇百怪,因为不管怎么说,组件虽然可视为个体或是实例,但也是一种抽象。

组件的划分

目前来说,通常的组件划分可从两个角度来进行:

1. 视觉和交互上是一个完整的组件。
这里挑了个视频网站,如图:

image

通常我们第一眼看上去可区分出独立的视图和功能的,则可以列为这种组件的划分。或许也可以成为区块,是为视觉和交互上完整的组件。

2. 写代码的时候,可重复的内容即可视为一个组件。

还是这个网站,我们看:

image

这里,我们能看到这种卡片形式的内容,存在页面中的各个地方。

通常这种情况下,我们将其封装成一个简单的组件,包括图片+文字描述,当然还有一些简单的内容,可通过配置的方式来调整。

这样,我们就能在很多地方,都能使用这个组件。这种方式的组件划分,在理解上或许没有从业务上划分的直观,但从另一种角度看,哪种方式效率高一些也不一定呢。

Tips.
在一个团队内,最好是使用一种方式来进行划分。因为对于成员的相互配合和项目的维护来说,统一的规范是比较重要的。

组件的封装

个人认为,一个称职的组件,是以下形式的:

  • 组件内维护自身的数据和状态
  • 组件内维护自身的事件
  • 通过初始化事件(event、绑定的 scope 事件,传入数据),来初始化组件状态,激活组件
  • 对外提供配置项,来控制展示以及具体功能
  • 通过对外提供查询接口,可获取组件状态

笼统地概括下,就是我们尽量需要把组件进行隔离,拥有独立的个体空间,同时保持与外界适当的联系。

组件内维护自身的数据和状态

这个比较好理解,以上面的小卡片为例子:
image

这个小卡片,它维护着自己的数据:封面图、描述、头像、作者。还有一个初始的状态,就是目前我们看到的样子。
这些内容保存在组件自己的scope里,每个卡片组件都拥有自己的数据和状态。

组件内维护自身的事件

我们在把鼠标放在卡片上,随着鼠标的位置,顶部会有个小小的进度条,同时封面图会改变,如图:
image
image

每个小卡片都有自己mousemove事件。当然,这里面维护了一个鼠标位置的状态,同时根据鼠标位置来控制图片的展示。

通过初始化事件,来初始化组件状态,激活组件

组件的数据,大多数需要外界提供并传入,故可通过初始化事件来激活。

对外提供配置项,来控制展示以及具体功能

可以通过配置的方式,来控制组件的展示和功能。

我们看这个小卡片:
image

和上面的不一样,左下角展示的是视频时长,而不是头像和名字,这种我们可以通过配置的方式来控制。

通过对外提供查询接口,可获取组件状态

很多时候,组件独立维护着自身的数据和状态,但有些场景下,父组件或者应用需要知道组件当前的状态,故我们需要对外提供接口,以供查询。

像 Vue 中,便可以通过vm.$refs来获取子组件的实例。

组件的通信

当我们层层封装组件后,或许就会带来组件间通信的问题。从最简单的父子组件、到同胞组件、到几乎毫无关系的组件的通信,我们可以有不同的解决方法。

1. 事件监听。

简单的,我们可以直接通过事件监听来通信,全局或是局部的监听和触发。

但是使用频繁之后,我们会发现不好维护。为什么呢?因为你在追踪事件的触发和监听时,只能通过全局搜索对应的事件名称,这样数据流会有种到处乱窜、无法管理的感觉。

2. 对象数据。

我们也可以通过共享同一个对象的方式来管理。通过注入对象的引用,来在不同组件中获取相同的数据源。

这样有时候还会存在问题,当我们需要获取新的数据实例时,则需要手动去维护。当然,在 Angular 里,则是通过提供通用的依赖注入方式,配合树状的模块管理,可通过局部注入实例来获取共享或是隔离的数据。

3. 状态流管理。

这是通过流的方式管理状态,常见的状态管理工具 Vuex、Redux 等,都是这样的方式去管理。

即使是使用这样的流管理,也是通过对象数据的方式来管理。当然,加入了单向流动的方式,这样提高了维护性,因为你知道所有数据都从哪里流入、从哪里流出。

组件化程度

**组件的封装是会消耗维护性的,过度的封装会导致代码难维护,可读性也差。**所以我们需要根据项目的大小以及复杂度,来进行什么程度的封装,当然封装除去组件的封装,还可以是数据、样式、功能的封装。

尽管前端的框架或是能力一直在变化,但项目的维护始终是很重要的事情。不能因为说今天这套明天就不适用了,就不打算细细去做,我们要把思考和反省,贯穿在整个写码的人生中。

适度也是很重要的,个人认为好的架构是变化的,跟随着项目变化而变化,保持着拓展性和维护性。如果说我们只是为了抽象而抽象,那想必会把简单的事情复杂化,整个应用和代码会变得难以理解。适度的抽象很重要,但相比错误的抽象过程,没有抽象可能会更好。

结束语

回到文章开头,技术与业务是相关的,很多时候并不能简单地区分所谓技术需求和业务需求。同样的,业务需求也能写得很有意思,多多在里面掺杂些自己的想法吧,实践出真理~~
关于组件封装的,后面会有其他文章详细说明哒,这里介绍下目前的主要想法而已。

数据抽离与数据管理

托目前主流框架的福,我们能从事件驱动脱离,来到了数据驱动的世界,可以参考以前的《前端思维转变--从事件驱动到数据驱动》。在常常与数据打交道后,我们对组件的封装、配置化的**一步步地深入和拓展之后,对于数据和状态的管理,也慢慢地出现了一些思考。

应用数据抽离

在把数据与逻辑分离到极致的时候,你再看一个应用,会看到一具静态的逻辑躯壳,以及数据如灵魂般地注入到应用里,使其获得生命。

数据的抽离,其实与配置化的**有想通的地方,即把可变部分分离,然后通过注入的方式,来实现具体的功能和展示。

状态数据

在一个应用的设计里,我们可能会拥有多个模块,每个模块又各自维护着自己的某些状态,同时部分状态相互影响着,最终呈现出应用的整体状态。

这些状态,都可以通过数据的方式来表示,我们简单称之为状态数据。

怎么定义状态数据?最浅显的办法就是这些数据,可以直接影响模块的状态,如对话框的出现、隐藏,标签的激活、失活,长流程的当前步骤等,都可以作为状态数据。用在 Vue 里面,可能常见如 v-showv-if、以及其他状态判断逻辑。

我们的应用,大多数都是呈现树状结构,一层层地往下分解,直到无法分割的某个简单功能。同时,我们的组件也会呈现出来这样树状的方式,状态是跟随着组件维护,某个功能状态属于组件自己,最外层的状态则属于整个应用,当然这个应用也是组件的一种表示方式。

因此,我们的状态数据,也呈现一种树状的方式,与我们的组件相对应。就像 CSS 与 DOM 节点。

动态数据

我们还有很多的数据,如内容、个人信息等,都是需要我们从数据库拉取回来的。这种需要动态获取然后展示或是影响配置的一些数据,我们称之为动态数据。

动态数据不同于状态数据,并不会跟随着应用的生命周期而改变,也不会随着应用的关闭而消失。它们独立存在于外界,通过注入的方式进入应用,并影响具体的展示和功能逻辑。

和状态数据不一样,动态数据并不一定呈现为树状的形式。它可以是并行的,可以是联动关系,但是随着注入的地方不一样,最终在应用中形成的结构也会不一致。我们可以简单理解为每个动态数据都是平等的。

将数据与应用抽离

要怎么理解将数据与应用抽离呢?形象点形容,就像是我们一个公司,所有的桌子椅子装修和电脑都是静态的,它们相当于一个个的组件,同时每个办公室也可以是一个大点的组件或是模块。

那么在我们这个公司里:

  • 状态数据:椅子的位置、消耗的电量、办公室的照明和空调状态等
  • 动态数据:员工等各种人员流动

当然,公司里没有人员流动的时候,似乎就是个空壳。每天上班的时候,一个个的程序员就涌入公司,给公司注入灵魂,公司得以运作。

要说将数据和应用抽离,作用到这里大概是这个样子?

--------------------------------------------------------
                         公司
---------------------------  ---------------------------
|                         |  |  人           人        |
|                         |  |       人          人    |
|         办公楼          |  ||
|                         |  |    人     人     人  人 |
|                         |  |      人      人   人    |
---------------------------  ---------------------------

在公司正常运作的时候,则是这样的:

--------------------------------------------------------
                         公司
--------------------------------------------------------
|   人     人             人   人       人     人 人    |
|           人            人   人     人          人    |
|        人    人    办公楼   人   人          人       |
|     人    人                人  人     人     人  人  |
|     人     人         人      人     人      人   人  |
--------------------------------------------------------

当然,人不只是站在办公楼里面这么简单,更多的,人会与各种物件进行交互和交流,人与人之间也会相互影响。但是这样简单的管理,很容易造成公司的混乱,所以我们会把人员有规律有组织地分别注入到每个办公室、隔间里面。

这就是我们要做的,不只是如何划分数据、将数据与应用抽离,我们还需要将其有规律地管理。所以,这大概是我们接下来的要讲的内容。

应用数据管理


我们知道哪些数据需要抽离、如何将数据抽离出来,同时,我们还需要知道,这些数据在抽离出来之后,该怎么去进行管理。

数据的流动

数据在注入到我们的应用中后,并不只是简单地存在。它可能会影响应用的状态、同时也会影响其他同样注入的数据。

数据与数据之间的交互,其实在某方面相等于我们组件之间的通信,包括但不限于以下的一些方式:

  • 事件通知
  • 共享对象
  • 单方向流动

事件通知

事件的监听和触发机制,在许多的场景下都能适用,如浏览器点击、输入框的输入操作,则是典型的事件机制。而在很多时候,我们也可以通过事件通知的方式,来进行数据间的交互,如 Websocket 机制。

事件通知机制很方便,可以随意地定义触发的时机,也可以任意地点使用监听或是触发。

但事件机制的弊端也是很明显,就是每一个事件的触发对应一个监听,关系是一一对应。在整个应用中看,则是散落在各处,随意乱窜的数据流动。需要定位的时候,只能通过全局搜索的方式来跟踪数据的去向。

image

当然,也有些人会定义一个中转站,所有的事件数据流都会经过那,这样的维护方式会有所改善。

imgae

类似这种,额,可能会好一些?

共享对象

共享对象是很简答的一种方式,当我们需要多个地方使用相同的数据,我们就把它们放置在一个地方,大家都去那理获取和更新。

image
(额请叫我灵魂画家)

通过注入对象的引用,来在不同组件中获取相同的数据源。当然这样的使用方式,需要考虑锁的问题(当然单线程的 JS 里面这样的情况比较少)。

同时,很多时候我们在定义一个对象,有时候想要某些地方共享,有时候又想要获取一个全新独立的对象。这种情况下,我们需要考虑怎样去维护一套这种数据与实例。

单方向流动

给数据的流动一个方向,则可以方便地跟踪数据的来源和去处。通过流的方式管理状态,常见的状态管理工具 Vuex、Redux 等,都是这样的方式去管理。

image

当然,赋予数据流方向,但是数据的存放呢,可以通过共享对象的方式,也可以维护一个数据中心,所有的数据变更方式、触发逻辑、影响范围,都在数据中心里面管理和维护。

树状作用域

很多时候,我们的应用通过一层层地封装和隔离,最终会呈现为树状。

我们可以根据组件的树状作用域,结合共享对象的管理,来注入树状的数据结构。典型如 Angular 里,则是通过提供通用的依赖注入方式,配合树状的模块管理,可通过局部注入实例来获取共享或是隔离的数据。

image

适度的管理

与组件的封装和配置化相似,数据的抽象、抽离,也是需要适度的。

当我们的应用很小,只有简单的功能的时候,我们甚至不需要对这些状态、数据什么的进行特殊的管理,甚至一个简单的变量就可以搞定了。随着应用组件数量变多,我们开始有了组件的作用域,当组件需要通信,我们可以通过简单的事件机制、或是共享对象的方式来进行交互。

当我们的项目越做越大,要在上百的状态、上万的数据里要按照想要的方式去展示我们的应用,这时候一个状态管理工具则可以轻松解决乱糟糟的数据流问题。

结束语

对数据的抽离和管理,也越来越成为我们在项目架构中需要考虑的部分。应用状态数据的管理,其实里面会有很多的设计模式。或许这块过于抽象,这篇文章也未能表达出最好的想法。但是对于设计模式,我们可以先了解下,在实战的时候,你会不自觉地想起来,啊原来这就是哪个设计模式的使用场景呀~

vuepress

我也在用vuepress输出个人沉淀,但是侧边栏和下方的图片不知道怎么弄。可以看看你vuepress的源码吗

对话抽象

我们都知道,对于计算机来说,与外界交互的流程大概是输入 => 处理 => 输出。不妨试试将目光拉长,世界上的种种事物都符合这样的抽象模型。再拓展看看,我们的世界也能进行这样的抽象。

本篇文章,主要用于来开开脑洞。

单体模型

个体

世上万物,皆是相互依赖而生。拿我们人类来说,既是独立的个体,同时也免不了与其他人或事物的交流。我们听到的、看到的、感受到的、触摸到的,经过复杂的神经传递、处理,然后反馈到身体的各个部位,再分别作出反馈。

上面这一段,我们来分析看看:

function 人体(听觉, 视觉, 触觉, ...其他感觉){
    传递(听觉, 视觉, 触觉, ...其他感觉);
    反馈 = 处理(听觉, 视觉, 触觉, ...其他感觉);
    return 反馈;
}

我们来抽象一下:

function 个体(输入){
    输出 = 处理(输入);
    return 输出;
}

我们仔细观察,可以发现这个模型适用于任何事物,当然至于事物存在的主观性和客观性哲学问题这里暂不讨论。

群体

在这里,群体依然可以抽象为个体。

  1. 宏观角度

宏观上看,我们可以忽略个体之间的影响,或者说个体之间的影响并不能影响整体,我们可以得到:

群体 = 个体

此时我们可以知道,群体符合上方所述的个体模型。

  1. 微观角度

微观上看,群体由多个个体组成,其中也包括了个体间的相互作用:

群体 = 个体 + ... + 个体 + 各个个体间的相互作用
// 1. 假设个体间满足串行关系
function 群体(输入){
    个体1输出 = 个体1处理(输入);
    个体2输出 = 个体2处理(个体1输出);
    ...
    个体n输出 = 个体n处理(个体n-1输出);
    return 个体n输出;
}

// 2. 假设个体间满足并行关系
function 群体(输入){
    // 拆分给各个个体的输入,分别处理
    for(var i = 0; i < 个体.length; i++){
        个体i输入 = 获取个体输入(输入, i);
        个体i输出 = 个体i处理(个体i输入)
    }
    // 汇总输出
    总输出 = 汇总([...各个个体的输出]);
    return 总输出;
}

不管群体中个体间是并行、串行关系,还是其他更加复杂的关系,最后我们总会发现,微观角度的群体也总能满足个体模型,我们将其称作“单体模型”:

// 单体模型
function 单体(输入){
    输出 = 处理(输入);
    return 输出;
}

交互

前面也说了,作为独立的个体,同时个体与个体之间也存在交流,可以理解为交互。
我们抽象完单体,现在来看看单体间的交互(交流),首先我们从人机交互开始。

人机交互

作为一个程序员,一天几乎有一半时间都在电脑面前,我们将人和电脑的互动归类为“人机交互”。

简单来说,人机交互无非包括两个过程:

  • 人 => 机:通过操作对机器进行输入,例如从键盘输入内容、用鼠标点击某些元素等
  • 机 => 人:机器拿到输入内容,根据程序进行计算,可通过显示器、声音设备进行输出

image

以上的过程我们可以抽象为两两单体之间的交互:

function (输入){
    输出 = 处理(输入);
    return 输出;
}

function (输入){
    输出 = 处理(输入);
    return 输出;
}

// 1. 输入从机开始
人的输入 = (输入)
机的输入 = (人的输入)

// 2. 输入从人开始
机的输入 = (输入)
人的输入 = (机的输入)

我们得到一个鸡生蛋蛋生鸡的模型。

多体交互

多体交互比两体交互稍微复杂,因为每个单体接收的输入是所有其他单体汇总的输入,而每个单体提供的输出也将成为所有其他单体输入直接或间接的影响。

这段话是不是有些似曾相识,因为它部分符合我们在前面讨论的微观群体的模型,也就是说我们在群体中:

const 单体1输入 = 汇总(...其他n-1个单体的输出);

我们发现,汇总这个处理,与单体的模型非常相似,我们将其定义为一种交互介质

function 交互介质(单体输出){
    单体输入 = 处理(单体输出);
    return 单体输入;
}

这个模型与单体模型完全一致,也就是说,交互介质也是单体的一种,这时候我们人机交互过程图调整为:

image

也就是说,群体 = 单体 + ... + 单体这条公式是正确的,我们可以将群体直接视为多个单体的简单相加。

虽然我们知道,所有的事物(包括交互介质)都可抽象为一个个的单体,但一个个单体之间是怎么衔接起来呢?

我们将所有的输入和输出抽象为“”,对于流的传递(交互),我们使用熟悉的事件监听的方式:

function 监听器(输入流, 单体) {
    输入流.变更 = (输入流) => {
        单体(输入流)
    };
}

而我们单体的输出流,通常也是其他单体的输入流,于是我们能看到整个流的流动:

单体n输出 = 单体n(...(单体2(单体1(输入))));

如果说我们需要流式处理我们的流,使用典型的jQuery链式调用方式,我们可以在创建单体的时候添加对于单体方法:

function 添加流处理(...n个单体){
    // 注册流的链用方法
    n个单体.forEach((单体, n) => {
        .单体n = function() {
            this = 单体(this);
            return this;
        }
    })
}

单体1 = 单体();
...
单体n = 单体();
添加流处理(单体1, ..., 单体n);

此时我们将流调整为第一人称,则有比上面稍微优雅的方式:

输出流 = 输入流.单体1().单体2().[...].单体n();

光与电的世界

我们来将抽象模型实例化。假设我们的世界,驱动能量为光和电,即各个单体的输入输出都只有光和电。
注意,这里我们将光和电视为两种完全独立的能源,可作为流的一种。

光和电的单体

上面我们假设,单体和单体之间的交流通过电、光,所以我们得到这样的单体:

function 单体(输入){
    输入光 = 输入.;
    输出光 = 处理光(输入光);

    输入电 = 输入.
    输出电 = 处理电(输出电);

    // 输出
    return {
        : 输出光,
        : 输出电
    };
}

从上面推出群体可抽象为个体,则我们的世界也是一个承接光电流的单体,而它对光和电的处理方式主要表现为传播(减弱或增强):

function 世界(输入){
    输入光 = 输入.;
    输出光 = 传播(输入光);

    输入电 = 输入.
    输出电 = 传播(输出电);

    // 输出
    return {
        : 输出光,
        : 输出电
    };
}

我们假设世界是没有输入的,因此我们需要手动添加能量的来源:

function 创建世界(){
    const 输入 = {};
    输入. = 添加光源();
    输入. = 添加电源();
    return 世界(输入);
}

说白了,我们创建了世界这样一个特殊的单体,同时给世界提供光和电的能量源。

光和电的传播机制

从前面调整的流的第一人称链式用法,我们不难得到光和电的传播机制都符合:

输出流 = 输入流.单体1().单体2().[...].单体n();

而流从何处开始输入,又将在何处输出,取决于哪一段是你所需要观察的而已。

我们创建一个世界之后:

  1. 流的规则设定好,流开始流动;
  2. 流的流动,表现为各单体对流的 输入 => 处理 => 输出;
  3. 单体和世界、单体间通过流来进行交互,表现为 输入 => 处理 => 输出;
  4. 其中每一段流作为研究对象,抽象成单体或者群体进行分析。

当然,这与现实世界中的光和电的传播十分相似:

  1. 对不同物质会产生不同的效果(单体的处理)。
  2. 会在经过不同的物质之后,增强或是减弱(输入流和输出流)。

结束语

以上尽是一些遐想,如果说太过于抽象,其实把流换成我们熟悉的程序中的数据流,或许容易理解得多。
假设我们代码之中,一切可作为流,DOM的事件流、http请求流,然后通过各种函数(单体)来驱使我们的流流动,也不妨为一种有趣的角度。

前端构建大型应用

与其说是构建大型应用,或许更多地是常用的应用构建吧。很多时候我们的项目在搭建的时候通常都不会定位为大型项目,但我们还是需要考虑到拓展性,或者说是在当项目开始变得较难维护的时候,要进行调整的一些方面。

项目设计

在项目开始之前,我们需要做一系列的规划,像项目的定位(to B/C)、大小,像框架和工具的选型,还有很重要的一点是,项目和团队规范。

框架选择

通常来说,框架选择是准备项目的第一步。

说到框架,目前主流三大框架 Angular、React 和 Vue,先从个人理解来看看这三个框架。

Angular

这里的 Angular 是指 Angular 2.0+ 版本,v1.0 我们通常称之为 AngularJS,目前已经不更新了,建议大家还是使用 Angular。

Angular 相对 React 和 Vue,最初的设计是针对大型应用来进行的。要是你认识 JAVA 的话,像依赖注入这一套你会觉得很熟悉。当然到了 v2.0 以上的版本由于加入了很多的语法糖,看起来 AngularJS 和 Angular 相差很远,但是最核心的依赖注入模式还是相似的。

关于 Angular 各个版本的对比,大家可以参考下《谈谈Angular--从Angular1到Angular4》 以及《重新认识Angular》

项目中使用 Angular,最大的体验感受则是项目有完备的结构和规范,新加入的成员能很快地通过复制粘贴完成功能的开发。身边有人说过,好的架构设计,能让高级程序员和初入门的程序员写出相似的代码,这样对于整体管理和项目的维护有非常好的体验。

至于 Vue 和 React,它们更多是小巧的模板框架,也可以通过灵活搭配路由、状态管理等工具,达到大型应用的管理。目前来说,社区也有比较好的解决方案,官方也有出相关的脚手架来快速构建应用。

很多人说 Angular 难上手,其实主要在于开始的项目搭建、以及 Angular 独有的一套设计方案的理解。但是依赖注入的设计方式,我们几乎不用考虑很多数据和状态管理的问题。当然脏检查的方式曾经也带来性能问题,后面在加入树状的模块化、Zone.js 之后,即使没有虚拟 DOM,性能也是有大大的提升。

React

本人接触的 React 项目不是很多,但是 jsx、虚拟 DOM、函数式编程的设计,带来的震撼和冲击还是很大的。

React 相对 Angular 最大的优势是轻量,或许其实这么比较不大对,因为 React/Vue 和 Angular 不一样,Angular 是整套的解决方案,而 React/Vue 则是项目搭建中灵魂使用的前端模板工具。

Angular = React + react-router + Redux/Flux/Mobx = Vue + vue-router + Vuex/Redux

虚拟 DOM 主要解决了什么,它的原理又是怎样的,这些会涉及到浏览器的页面渲染原理,包括 DOM Tree、CSS Ruler Tree、Rendering Tree、Repaint、Reflow等等,你需要去理解虚拟 DOM 为何能带来性能的提升。

类似这样的,你会在使用 React 的时候,接触到很多好的设计,去引领你进行更深入的思考。函数式编程的方式,也会不同程度地拓展你的思考方式,遇到问题的时候,能有更多的解决办法。

至于社区建设,其实三大主流开源框架的社区都相当完善了。

Vue

如果你熟悉 Angular 以及 React,你会发现 Vue 的使用,其实很多地方像是两个的结合体。

Vue 最大的特点是上手简单,不管是框架的设计和文档,都对新手极其友好。但是这并不代表它只是个简单的框架,当你需要实现一些更加深入的自定义功能时,你会发现它其实也有很好的功能支持。如果你还认为它只是把 Angular 和 React 的优势结合,在你深入使用甚至阅读源码的时候,你会慢慢发现里面的一些自己的思考,真的是个很棒的框架。

如果说你是个新手,那么 Vue 会是较好的选择。相比另外两个框架,Vue 最初的社区缺陷现在也早已不再是问题了,相关的脚手架、配套工具也都很完善。

开源框架?

使用开源框架的好处是,有着完整详细的文档,同时有问题也能通过 issues 和 Stack Overflow 来查找。

更多时候,我们选择一个框架,需要考虑项目大小、定位。**技术选型更多的在于团队,你要考虑这个团队的能力、大家对各个框架的熟悉程度、是否有强烈的倾向。**或者有能力的团队,也可以选择相对小众的框架。

当然也有些小伙伴喜欢自己造轮子,不过你们要记得,造轮子是要负责任的,你需要提供友好的文档和 API 给他人,不然对项目的维护来说,简直就是毁灭性的体验。

还有在生产环境,尽量使用 release 版本吧。那段将 Angular2-beta 升级到 Angular4-rc 版本的日子,真的不堪回想。

有些人喜欢捣鼓新东西,个人的体验是,我们尽量在对这些新技术有较好地把握之后,再尝试一点点加入我们的项目里。项目尤其是工程项目,大多数是解决某些问题,我们需要在满足业务和项目维护性的同时,来做一些新的尝试。

项目代码结构

个人认为,好的项目代码结构会大大提升项目的维护性。同时我们可以提供友好的说明,以便其他成员理解项目和快速定位。

这里贴出个人比较偏好的结构:

# Angular
│
├── src/                              # 项目代码存放位置
│   │
│   ├── app/                          # app相关代码
│   │   ├── modules/                  # 业务模块
│   │   ├── shared/                   # 公用组件/服务等
│   │   └── container/                # Angular应用入口文件(路由、启动模块等)
│   │
│   ├── assets                        # 相关资源
│   ├── environments/                 # 环境相关配置
│   │
│   ├── index.html                    # 主页面
│   └── main.ts                       # angular启动文件
│
├── dist/                             # 存放编译打包生成文件
├── e2e/                              # e2e测试相关文件
├── .gitignore                        # Git上传相关配置
├── package.json                      # npm相关配置
├── README.md                         # 说明文档
└── xxx.json/xxx.js/.xxxx             # 其他配置文档
# Vue
│
├── src/                              # 项目代码存放位置
│   │
│   ├── app/                          # app相关代码
│   │
│   ├── components/                   # 公用组件
│   ├── router/                       # 路由配置
│   ├── store/                        # store
│   │
│   ├── index.html                    # 主页面
│   ├── main.js                       # 启动文件|
│   └── ...                           # 其他文件
│
├── dist/                             # 存放编译打包生成文件
├── config/build/                     # 项目构建相关配置
├── .gitignore                        # Git上传相关配置
├── package.json                      # npm相关配置
├── README.md                         # 说明文档
└── xxx.json/xxx.js/.xxxx             # 其他配置文档

其实有一点比较重要,就是公共组件、工具等同类的文件,放置一起维护会比较好。而且还有个小 tips,我们可以在搭建项目的时候,在 README.md 里面描述下该项目下的代码和文件结构。

代码流程规范

代码规范其实是团队合作中最重要的地方,使用相同的代码规范,会大大减少我们接手别人代码时候卧槽的次数。

好的写码习惯很重要,命名习惯、适当的注释,会对代码的可读性有很大的提升。但是习惯是每个人都不一样,所以在此之上,我们需要有这样统一的代码规范。

一些工具可以很好地协助我们,像 Eslint、Tslint等,加上代码的打包工具协助,可以把一些规范强行标准化,来获取代码的统一性。还有像 prettier 这样的工具,能自动在打包的时候帮我们进行代码规范化。

除了这些简单的什么驼峰啊、全等啊、单引双引等基础的规范,其实更重要的是流程规范。最基础的是改动公共库或是公共组件的时候,需要进行 code review。通常我们使用 Git 维护代码,这样在合并或是版本控制上有更好的体验。

但其实最重要的还是沟通,沟通是一个团队里必不可少同时很容易出问题的地方,要学会沟通方式、表达方式。

大型应用优化

说到大型应用,其实像页面加载性能、构建打包等地方,我们都可以适当进行优化。

路由管理

路由管理其实主要是当我们的项目变大方便管理,同时为了项目体验问题而引入的解决方案。毕竟我们产品设计都比较成熟,对用户来说刷新页面会丢掉页面状态,这样的体验是在是太糟糕了。

我们在规划项目路由的时候,会推动我们对项目业务、功能区块化有更加深入的认识和理解。同时对我们的项目结构规划也有很大的帮助,我们可以根据路由来放置我们的代码文件,有了这样的约定,我们在维护他人代码的时候,便能迅速地定位到对应的位置。

路由管理现在很多框架都有配套的工具库,已经有很多完善的解决方案了,这里不多说。

抽象和组件化

在我们开始写重复的代码、或是进行较多的复制粘贴的时候,大概我们需要考虑对组件进行适当的抽象了。

好的抽象能大量减少重复代码,同时对项目整体有更深入的理解。坏的抽象会增加项目的复杂度,同时降低了代码的可读性和维护性。所以关键在于适度,好的办法是结合产品和业务来进行抽象,例如一个播放器组件、日历组件、快速导航栏、快捷菜单等组件封装,便于多次使用。

而组件的抽象相关,可以参加《 一个组件的自我修养》

同时,我们也需要把一些相同的方法抽离,封装成通用的工具库,像常用的时间转换、字符串处理、http 请求等,都可以单独拎出来维护。

状态和数据管理

我们的应用里,多数会面临组件的某些状态和数据相互影响、相互依赖的问题。

现在也有比较成熟的解决方案和状态管理工具,像 Vuex、Redux、Mobx 等,我们需要结合自身的框架和业务场景来使用。像父子组件的交互、应用内无直接管理的数据状态共享、事件的传递等,也都需要结合实际适当地使用。

代码打包

当我们的应用变得很大,为了提升首屏加载的体验,我们需要对代码进行分块打包。

一般来说,不同的框架有不同的异步加载解决方案,同时可以结合打包工具(Webpack等)进行分块打包。我们可以把首屏相关的东西打包到 bundle,其他模块分块打包到 chunk,来在需要的时候再进行加载。

路由异步加载

通常情况下,我们会结合路由进行分块打包,路由管理工具大部分都支持异步加载。

我们可以根据自己需要,来打包成多个文件,在路由进入的时候才获取和加载。Vue 的话可参考《Vue2使用笔记17--路由懒加载》,打包效果像这样:

image

Webpack 分块打包

使用 Webpack 的话,可以用:

  • CommonsChunkPlugin:提取代码中的公共模块,然后将公共模块打包到一个独立的文件中,以便在其他的入口和模块中使用
  • ExtractTextPlugin:可以将样式或其他从 js 中抽出,生成单独的.css样式文件
  • require.ensure()
    • webpack 在编译时,会静态地解析代码中的require.ensure(),同时将模块添加到一个分开的 chunk 当中
    • 这个新的 chunk 会被 webpack 通过 jsonp 来按需加载

Source map

这里需要讲一下,Source map 就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大方便。

关于 Source map,可参考阮大神的《JavaScript Source Map 详解》

在开发环境下,还能通过 Chrome 匹配源文件进行在线 debug 和修复源码。大家也可以自行搜索下,尤其我们需要在浏览器上直接调试 CSS 并生效的时候,体验真的很棒。

Tree-shaking

不知道大家熟悉 Tree-shaking 不,我们在引入一些开源代码或是公共库的时候,其实大部分时间我们都只是使用其中里面的一小部分代码。

Tree-shaking 通常指按需加载,即没有被引用的模块不会被打包进来,减少我们的包大小,缩小应用的加载时间,呈现给用户更佳的体验。

最初是 Rollup 提出并实现。Rollup 静态分析代码中的 import,并将排除任何未实际使用的代码。这允许我们架构于现有工具和模块之上,而不会增加额外的依赖或使项目的大小膨胀。

同时在 Webpack 2 里也加入了 Tree-shaking 新特性。至于目前的一些情况,也可以参考下《你的Tree-Shaking并没什么卵用》这篇文章。

编写可测试代码

测试的重要性不用多说,有了测试,我们每次功能调整和重构的时候,心会踏实很多吧。

但是目前大部分情况是,项目中功能的快速迭代、开发工作量饱满等原因,导致甚至单元测试这种都很少编写。Emmmmm。。。所以这里不多讲述,因为本人也没有太多经验。

结束语

项目的维护永远是程序员的大头,多是前人种树后人乘凉。但是很多时候,大家会为了一时的方便,对代码规范比较随意,就导致了我们经常看到有人讨论“继承来的代码”。
或许相比新技术的研究和造轮子,有个好的写码习惯、提高项目维护性并不能带来短期的利益,但是其实身为程序员,还是要对自己写的东西负责任的呢。

兄弟你也太NB了。

仅仅点start我已经无法表达我的respect,能坚持写博客这么久而且是高质量内容产出太不容易了!!
我一定要留个issue!【手动狗头】

关于Typescript

你了解Typescript吗

什么是Typescript

TypeScript是JavaScript的超集,带来了诸多新特性:

  • 可选的静态类型
  • 类型接口
  • 在ES6和ES7被主流浏览器支持之前使用它们的新特性
  • 编译为可被所有浏览器支持的JavaScript版本
  • 强大的智能感知

Typescript特性

  • 可选静态类型

类型可被添加到变量,函数,属性等。这将帮助编译器在App运行之前就能显示出任何潜在的代码警告。

给JavaScript加上可选的类型系统,很多事情是只有静态类型才能做的,给JavaScript加上静态类型后,就能将调试从运行期提前到编码期,诸如类型检查、越界检查这样的功能才能真正发挥作用。TypeScript的开发体验远远超过以往纯JavaScript的开发体验,无需运行程序即可修复潜在bug。

  • 支持使用ES6和ES7的新特性

在TypeScript中,你可以直接使用ES6的最新特性,在编译时它会自动编译到ES3或ES5。

  • 代码自动完成,代码智能感知

ts与js

TS是一个应用程序级的JavaScript开发语言。
TS是JavaScript的超集,可以编译成纯JavaScript。
TS跨浏览器、跨操作系统、跨主机,开源。
TS始于JS,终于JS。遵循JavaScript的语法和语义,方便了无数的JavaScript开发者。
TS可以重用现有的JavaScript代码,调用流行的JavaScript库。
TS可以编译成简洁、简单的JavaScript代码,在任意浏览器、Node.js或任何兼容ES3的环境上运行。
TypeScript比JavaScript更具开发效率,包括:静态类型检查、基于符号的导航、语句自动完成、代码重构等。
TS提供了类、模块和接口,更易于构建组件。

参考

为什么是Typescript

大型项目常见问题

1. 类型不明确,甚至在使用中转换。

var type = 1;
type = 'abc';

2. 对象成员属性不明确,使用容易出错。

var obj = {a: 123, b: 323};
console.log(obj.c);

3. 接口返回内容和格式不明确。

ajax('url', function (json){
    json.result ??
}

4. 接手代码注释不多,相关变量命名不规范,变量类型、接口类型等均难以debug。

5. 重构代码、重命名符号需要改动太多相关文件。

谁在使用Typescript

框架:Angular
工具:TSLint
编译器:VSCode
工具库:RxJSUI-ROUTER
UI:ANT Design React UI库
APP:Reddit

Angular说

1. TypeScript 拥有很好的工具。

它提供了先进的自动补全功能,导航,以及重构。有这样的工具几乎是开发大型项目的必要条件。没了这些工具,修改代码的恐惧将会导致该代码库在一个半只读(semi-read-only)状态, 并且使大规模重构变得极具风险,同时消耗巨大资金。

2. TypeScript 使抽象概念明确。

一个好的设计在于定义良好的接口。支持接口的语言使得表达想法变得更加容易。
不能清楚地看到界限,开发者开始依赖具体类型而不是抽象接口,导致了紧密耦合。

3. TypeScript 使代码更易阅读和理解。

Reddit说

  1. 要支持强类型。
  2. 要有很好的配套工具。
  3. 已经有了成功案例。
  4. 我们的工程师可以很快上手。
  5. 能同时工作于客户端和服务器。
  6. 有优秀的类库。

Typescript vs Flow

Typescript是JavaScript的强类型版本。
Flow是通过一组可以添加到JavaScript的注解,然后通过工具检查正确性。
Flow的类型注解能自动的被Babel移除。
与TypeScript相比,Flow在类型检查中做得更好。
Typescript是强类型,能使代码有更少的类型相关bug,更容易构建大型应用,还有着丰富的生态系统。

TypeScript的一大加分项就是其生态系统,TypeScript的支持库实在是太棒了。

并且还支持目前流行的编辑器,比如VSCode, Atom和Sublime Text。
此外,TypeScript还支持解析JSDoc。

为什么使用Typescript

1. 提供了先进的自动补全功能,导航,以及重构工具。

构建丰富的开发工具从第一天起就成为了TypeScript团队的明确目标。
这也是为什么他们构建了编程语言服务,使得编辑器可以提供类型检查以及自动补全的功能。那么多的编辑器都对TypeScript有极好的支持,就是因为TypeScript提供了编程语言服务。

2. 是JavaScript的超集,从JavaScript迁移方。

从JavaScript迁移到TypeScript不需要经过大改写。可以慢慢的、一次一个模块的迁移。

随便挑选一个模块,修改文件扩展名.js.ts,然后逐步添加类型注释。当你完成了这个模块,再选择下一个。
一旦整个代码库都被类型化,你就可以开始调整编译器设置,使其对代码的检查更加严格。

3. 支持接口,抽象设计。

在一个静态类型的编程语言中,使用接口来定义子系统之间的界限。

4. 类型的支持,使代码更易阅读和理解。

我们不需要深入了解代码的实现,也不需要去阅读文档,就可以更更好地理解代码。

5. 生态系统完善,支持库完备,已有不少使用TypeScript的成熟项目。

参考

使用Typescript

关于Typescript的语法,更多的可参考官方文档,这里只列出常用的:基础类型、接口和类。

基础类型

TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型使用。

// 布尔值
let isDone: boolean = false;

// 数字
let decLiteral: number = 6;

// 字符串
let name: string = "bob";

// 数组常用
// 在元素类型后面接上 []
let list: number[] = [1, 2, 3];

// 数组泛型,Array<元素类型>
let list: Array<number> = [1, 2, 3];

// any类型常用于对现有代码进行改写
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;

// void类型像是与any类型相反,它表示没有任何类型
// 函数没有返回值
function warnUser(): void {
    alert("This is my warning message");
}

// 默认情况下null和undefined是所有类型的子类型
// 可以把null和undefined赋值给各种类型的变量
let u: undefined = undefined;
let n: null = null;

接口

TypeScript的核心原则之一是对值所具有的结构进行类型检查。
它有时被称做“鸭式辨型法”或“结构性子类型化”。

在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

interface SquareConfig {
    color: string;
    // 可选属性
    width?: number; 
    // 指定属性 
    type: 1 | 2 | 3;
    // 只读属性 
    readonly x: number; 
    // 函数类型
    getArea(x: number): number; 
}
  • 接口继承
interface Shape {
    color: string;
}

// 接口继承
// 此时Square同时具有两个属性
interface Square extends Shape {
    sideLength: number;
}

ECMAScript 6开始,JavaScript程序员将能够使用基于类的面向对象的方式。

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

当接口继承了一个类类型时,它会继承类的成员但不包括其实现。
接口同样会继承到类的private和protected成员。

公共,私有与受保护的修饰符:

  • public(默认): 可以自由的访问程序里定义的成员
  • private: 当成员被标记成private时,它就不能在声明它的类的外部访问
  • protected: protected修饰符与private修饰符的行为很相似,但protected成员在派生类中仍然可以访问
  • readonly: 将属性设置为只读的,只读属性必须在声明时或构造函数里被初始化
class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name)
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // error

在Typescript中,可以使用ES6很多新的特性,其中类Class也是ES6特性之一。包括gettersetter,其实都是ES6而不是Typescript的特性。
publicprivateprotected等,则是Typescript中增加的。

声明文件

大多数情况下,类型声明包的名字总是与它们在npm上的包的名字相同,但是有@types/前缀:

npm install -D @types/node

这里我们参考node.d.ts中的require,在我们在typescript中使用require的时候,若无安装@types/node或是自己声明,会报错的:

// 声明require
declare var require: NodeRequire;
interface NodeModule {
    exports: any;
    require: NodeRequireFunction;
    id: string;
    filename: string;
    loaded: boolean;
    parent: NodeModule | null;
    children: NodeModule[];
}

项目配置

tsconfig.json:文件中指定了用来编译这个项目的根文件和编译选项。
tslint.json:规则定义。

// 常见tsconfig.json
{
    "compilerOptions": {
        "baseUrl": "src", // 根路径,常在使用paths时候结合使用
        "target": "es6", // 目标js版本,当需要承接jsx的时候可设为"es6",常设为"es5"
        "jsx": "preserve", // 保留jsx的处理,常用在使用jsx时
        "module": "commonjs",
        "sourceMap": true,
        "emitDecoratorMetadata": true, // 使用装饰器
        "experimentalDecorators": true, // 使用元数据
        "lib": [
            "dom",
            "es7",
            "es6"
        ],
        "paths": {
            "utils": "utils"
        }
    },
    "exclude": [
        "node_modules"
    ]
}

项目迁移

常用迁移步骤:

  1. 安装依赖(typescript / ts-loader / tslib等)
  2. 文件重命名(.js => .ts | .jsx => .tsx
  3. 添加tsconfig.jsontslint.json
  4. 调整Webpack配置(resolve.extensions / loaders
  5. 添加声明文件(@types/node等)

最后来个小故事

刚开始,项目比较小,我一个人写,每行代码我都能记得,每个变量我都知道是什么。

后来,我有了个小弟,他进来后熟悉项目全靠注释、README以及面对面讲解。我们开始愉快的合作节奏,分工进行与后台接口的对接,除了约定一些接口规范,我们通常只有一个初始版本的接口说明,联调中持续的更新并不能及时更新到文档或注释中。

再后来,又来了个小弟,我们开始了口口相传的说明。但是由于理解、表达和沟通的问题,效率开始下降。

我们每个人有各自的命名习惯,有的喜欢小驼峰,有的喜欢下划线,有的还爱用$。然后我们使用eslint,但是很多对象的属性、接口的类型等等,都无法解决。

我们使用不一样的编辑器,有VSCode,有WebStorm,有subline。

我们还经常出现接口调整,甚至是字段名调整的情况。

然后我们上了Typescript。

当时我们的框架是AngularJS(Angular1版本噢),但是也照样使用了ts。从js迁移到ts是其中一个小弟完成的(很牛逼噢),然后我们开始了制定一些规范,更新README说明。

后面的情况是:

我们对每个接口和数据对象定义interface,缺少相关的库类型定义也能从相关社区中找到。

不管我们使用怎样的编辑器,都能有很好的自动补全功能、导航工具。

接手相互的代码,能第一眼就能知道各个变量的类型,模块大致的作用等。

再也不怕经常性的调整接口,因为我们可以一键重构相同interface中的某字段。

接口的引入,使得我们对代码的抽象设计变得容易了,逻辑和架构也清晰了。

以上的这些这些,随着项目增大越发觉得舒服。

结束语

很多时候,当我们维护不同重量级的应用,或是在不同的场景中使用应用的时候,面对的架构选择往往是不一样的。
就像我们在很小的页面里使用redux会觉得繁琐,在数据类型不多的对象或接口中使用typescript会觉得没啥效果一样,个人还是认为,好的架构在能遇见拓展性的同时,不过度设计,恰到好处才是最棒的。

前端模板引擎

前端框架日新月异,而其中的数据绑定已经作为一个框架最基础的功能。我们常常使用的单向绑定、双向绑定、事件绑定、样式绑定等,里面具体怎么实现,而当我们数据变动的时候又会触发怎样的底部流程呢?

模板数据绑定

数据绑定的过程其实不复杂:

  1. 解析语法生成AST。
  2. 根据AST结果生成DOM。
  3. 将数据绑定更新至模板。

解析语法生成AST

抽象语法树(Abstract Syntax Tree)也称为AST语法树,指的是源代码语法所对应的树状结构。也就是说,对于一种具体编程语言下的源代码,通过构建语法树的形式将源代码中的语句映射到树中的每一个节点上。

其实我们的DOM结构树,也是AST的一种,把HTML DOM语法解析并生成最终的页面。而模板引擎中常用的,则是将模板语法解析生成HTML DOM。

捕获特定语法

生成AST的过程涉及到编译器的原理,一般经过以下过程:

  1. 语法分析。

语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等。
语法分析程序判断源程序在结构上是否正确,源程序的结构由上下文无关文法描述。语法分析程序可以用YACC等工具自动生成。

  1. 语义分析

语义分析是编译过程的一个逻辑阶段,语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查,进行类型审查。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息。
一般类型检查也会在这个过程中进行。

  1. 生成AST。

AST的结构则根据使用者需要定义,下面的一些对象都是本人根据需要假设定义的。

DOM元素捕获

最简单的,我们来捕获一个<div>元素,然后生成一个<div>元素。

例如我们可以将以下这样的DOM进行捕获:

<div>
    <a>123</a>
    <p>456<span>789</span></p>
</div>

捕获后我们或许可以得到这样的一个对象:

thisDiv = {
    dom: {
        type: 'dom', ele: 'div', nodeIndex: 0, children: [
            {type: 'dom', ele: 'a', nodeIndex: 1, children: [
                {type: 'text', value: '123'}
            ]},
            {type: 'dom', ele: 'p', nodeIndex: 2, children: [
                {type: 'dom', ele: 'span', nodeIndex: 3, children: [{type: 'text', value: '456'}]},
                {type: 'text', value: '789'}
            ]},
        ]
    }
}

原本就是一个<div>,经过AST生成一个对象,最终还是生成一个<div>,这是多余的步骤吗?不是的,在这个过程中我们可以实现一些功能:

  1. 排除无效DOM元素,并在构建过程可进行报错。
  2. 使用自定义组件的时候,可匹配出来。
  3. 可方便地实现数据绑定、事件绑定等功能。
  4. 为虚拟DOM Diff过程打下铺垫。

数据绑定捕获

这里我们拿来做例子的是,在Angular和Vue里面都有,是双大括号的数据绑定{{ data }}的语法。

在前面DOM元素捕获的基础上,我们来添加数据绑定:

<div>{{ data }}</div>

这么一个简单的数据,我们可以获得这样一个对象:

thisDiv = {
    dom: {
        type: 'dom', ele: 'div', nodeIndex: 0, children: [
            {type: 'text', value: '123'}
        ]
    },
    binding: [
        {type: 'dom', nodeIndex: 0, valueName: 'data'}
    ]
}

这样,我们在生成一个DOM的时候,同时添加对data的监听,数据更新时我们会找到对应的nodeIndex,更新值:

// 假设这是一个生成DOM的过程,包括数据绑定和
function generateDOM(astObject){
    const {dom, binding = []} = astObject;
    // 生成DOM,这里假装当前节点是baseDom
    baseDom.innerHTML = getDOMString(dom);
    // 对于数据绑定的,来进行监听更新吧
    baseDom.addEventListener('data:change', (name, value) => {
        // 寻找匹配的数据绑定
        const obj = binding.find(x => x.valueName == name);
        // 若找到值绑定的对应节点,则更新其值。
        if(obj){
            baseDom.find(`[data-node-index="${obj.nodeIndex}"]`).innerHTML = value;
        }
    });
}

// 获取DOM字符串,这里简单拼成字符串
function getDOMString(domObj){
    // 无效对象返回''
    if(!domObj) return '';
    const {type, children = [], nodeIndex, ele, value} = domObj;
    if(type == 'dom'){
        // 若有子对象,递归返回生成的字符串拼接
        const childString = '';
        children.forEach(x => {
            childString += getDOMString(x);
        });
        // dom对象,拼接生成对象字符串
        return `<${ele} data-node-index="${nodeIndex}">${childString}</${ele}>`;
    }else if(type == 'text'){
        // 若为textNode,返回text的值
        return value;
    }
}

我们来对上面的代码进行说明。

1. 根据节点信息生成对应的HTML string,也即getDOMString()方法。

这里我们只是简单完成了一种实现方式,根据节点生成DOM也有其他方式,例如使用.createElement().appendChild()textContent等等。

我们称通过生成HTML string的方式为字符串模版,同时我们将通过createElement()/appendChild()的方式生成DOM称为节点模版

2. 通过监听数据变更,同时根据绑定的数值获取对应节点,并进行局部更新。

在使用字符串模版的时候,我们将nodeIndex绑定在元素属性上,主要是用于数据更新时追寻节点进行内容更新。
在使用节点模版的时候,我们可在创建节点的时候,将该节点保存下来,直接用于数据更新。

当然,即使在字符串模版,我们也可以遍历一遍binding来获取所有绑定数据的节点并保存,这样就不用每次数据更新事件触发的时候重新进行获取,毕竟DOM节点的匹配也是会有一定的消耗的。

3. 无论是数据还是事件、属性、样式等的绑定,都可以通过相似的方法进行。

虽然这里我们只介绍了数据的绑定,但其实事件的绑定、属性和样式的绑定都可以用相似的方式进行,当然事件监听和事件的触发都是我们自己定义的,对于传递的内容都可以用自己想要的方式来传。

AST生成模版

生成模版的方法

我们在捕获得到一个AST树结构后,会将其生成对应的DOM。一般来说我们有这些方式:

  1. 字符串模版:使用拼接的方式生成DOM字符串,直接通过innderHTML()插入页面。
  2. 节点模版:使用createElement()/appendChild()/textContent等方法,动态地插入DOM节点,根节点使用appendChild()插入页面。
  3. 混合模版:使用createElement()/appendChild()/textContent等方法动态地插入DOM节点,但是根节点使用innderHTML()插入页面。

这几个有什么区别呢?

刚开始的时候,我们每次更新页面数据和状态,通常通过innerHTML方法来用新的HTML String替换旧的,这种方法写起来很简单,无非是将各种节点使用字符串的方式拼接起来而已。但是如果我们更新的节点范围比较大,这时候我们需要替换掉很大一片的HTML String

对于浏览器,这样的一次HTML String替换并不只是更新一些字符串那么简单。

浏览器的渲染机制

浏览器的一次页面渲染其实开销并不小,首先浏览器会解析三种文件:

  • 解析HTML/SVG/XHTML,会生成一个DOM结构树
  • 解析CSS,会生成一个CSS规则树
  • 解析JS,可通过DOM APICSS API来操作DOM结构树CSS规则树

CSS规则树DOM结构树结合,最终生成一个Render树(即最终呈现的页面,例如其中会移除DOM结构树中匹配到CSS里面display:none;的DOM节点)。其中,CSS匹配DOM结构的过程是很复杂的,曾经在机器配置不高的日子也会出现过性能问题。

**一般来说浏览器绘制页面的过程是:1.计算CSS规则树 => 2.生成Render树 => 3.计算各个节点的大小/position/z-index => 4.绘制。**其中计算的环节也是消耗较大的地方。

我们使用DOM APICSS API的时候,通常会触发浏览器的两种操作:RepaintReflow

**Repaint:页面部分重画,通常不涉及尺寸的改变,常见于颜色的变化。**这时候一般只触发绘制过程的第4个步骤。

**Reflow:意味着节点需要重新计算和绘制,常见于尺寸的改变。**这时候会触发3和4两个步骤。

所以我们在写页面的时候会注意一些问题,例如不要一条一条地修改DOM的样式(会触发多次的计算或绘制),在写动画的时候多使用fixed/absolute等(Reflow的范围小),等等。

回到话题,如果我们直接每次更新页面数据和状态,都使用innerHTML的方式,无疑会增加浏览器的负担,所以需要跟踪节点进行局部跟新。当然,innerHTML也有它的优势,那就是我们可以使用一个innerHTML替代很多很多的createElement()/appendChild()/textContent方法,这在我们较少使用数据绑定和更新的情况下高效得多。

模版数据更新

我们讲了模版生成AST,以及通过AST生成DOM、并进行数据绑定的过程,接下来说明下模版数据更新的过程。


数据更新监听

前面将数据绑定的时候,也讲了使用事件监听的方式监听数据更新。这里接着介绍一些其他的方式。

脏检测:在Angular中,并不直接监听数据的变动,而是监听常见的事件如用户交互(点击、输入等)、定时器、生命周期等。在每次事件触发完毕后,计算数据的新值和旧值是否有差异,若有差异则更新页面,并触发下一次的脏检测,直到没有差异或是次数达到设定阈值。

脏检测是Angular的一大特色。由于事件触发的时候,并不能知道哪些数据会有变化,所以会进行大面积数据的新旧值Diff,这也毫无疑问会导致一些性能问题。在Angular2版本之后,由于使用了zone.js对异步任务进行跟踪,把这个计算放进worker,完了更新回主线程,是个类似多线程的设计,也提升了性能。

同时,在Angular2中应用的组织类似DOM,也是树结构的,脏检查会从根组件开始,自上而下对树上的所有子组件进行检查。相比Angular1中的带有环的结构,这样的单向数据流效率更高,而且容易预测。

Getter/Setter:在Vue中,主要是使用Proxy的方式,在相关的数据写入时进行模版更新。

手动Function:在React中,通过手动调用set()的方式写入数据来更新模版。

使用Proxy或者是set()的时候,我们可以通过event emit或是callback回调的方法,来触发数据的计算以及模版的更新。

数据更新Diff

说到数据更新的Diff,更多的则是Diff + 更新模板这样一个过程。

在这个过程中,最突出的也就是虚拟DOM,它解决了常见的局部数据更新的问题,例如数组中值位置的调换、部分更新。一般来说计算过程如下:

1. 用JS对象模拟DOM树。

不知道大家仔细研究过DOM节点对象没,一个真正的DOM元素非常庞大,拥有很多的属性值。而其中很多的属性对于计算过程来说是不需要的,所以我们的第一步就是简化DOM对象。
我们用一个JavaScript对象结构表示DOM树的结构,然后用这个树构建一个真正的DOM树。

2. 比较两棵虚拟DOM树的差异。

当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。通常来说这样的差异需要记录:

  • 需要替换掉原来的节点
  • 移动、删除、新增子节点
  • 修改了节点的属性
  • 对于文本节点的文本内容改变

经过差异对比之后,我们能获得一组差异记录,接下里我们需要使用它。

3. 把差异应用到真正的DOM树上。

对差异记录要应用到真正的DOM树上,例如节点的替换、移动、删除,文本内容的改变等。

结束语

总的来说,一个前端模板引擎大致分为模板生成AST => AST生成模板 => 数据/事件/属性绑定的监听 => 数据变更Diff => 局部更新模板这些过程。当然上面的介绍以个人理解为主,部分源码验证为辅。
还是那句话,多思考多总结,不管结论是否正确,结果是否所期望,过程中的收获也会让人成长。

ES6/ES7好玩实用的特性介绍

本文介绍一些ES6/ES7好玩实用又简单的特性,或许对写代码的效率也有一定帮助噢。

ES6/ES7的出现已经有一段时间了,里面的一些新特性你们是否了解呢?本*年将结合自身的一些使用经历介绍一些简单实用的新特性/语法糖。
基础常用的一些如letconst等这里就不详细介绍了,关于ES6/ES7的一些具体说明介绍大家可以参考ECMAScript 6 入门


「解构」知多少

解构赋值

  • 数组和对象

数组的变量的取值与位置相关,而对象的属性与变量名有关。

// 数组
let [a, b, c] = [1, 'abc', [3, 4]];
// a = 1, b = 'abc', c = [3, 4]

// 对象
let { x, y } = { x: "a", y: 1 };
// x="a", y=1

数组和对象的解构赋值其实用得不多,毕竟这样代码阅读性可能不大好,尤其数组的解构赋值和变量顺序紧紧关联。

默认值

解构赋值允许指定默认值。我猜你们很多都用到对象的默认值,数组的用过吗?

// 数组
let [x, y = 'b', c = true] = ['a', undefined];
// x = 'a', y = 'b', c = true

// 对象
let {x, y = 5, z = 3} = {x: 1, y: undefined, z: null};
// x=1, y=5, z=null

let [x = f()] = [1]; // 这里的f()并不会执行
let [x = f()] = [undefined]; // 这里的f()会执行

从上面代码我们可以发现两点:

  1. ES6内部使用严格相等运算符(===),如果一个数组成员不严格等于undefined,默认值是不会生效的。
  2. 默认值是表达式时候,会遵守惰性求值(只有在用到的时候,才会求值)。

函数参数的解构

函数参数的解构就比较有趣了,当然应用场景会更多。

参数解构,同时设置默认值,再也不需要长长的if判断和处理了:

function plus({x = 0, y = 0}){
    return (x + y);
}

牛逼的点--拓展运算符(...)

数组和对象

别小看这三个点...,身为拓展运算符,它们还是很方便的。

// 数组
const [a, ...b] = [1, 2, 3];
// a = 1, b = [2, 3]

// 对象
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
// x = 1, y = 2, z = { a: 3, b: 4 }

这里面需要注意的是:

  1. 解构赋值必须是最后一个参数,否则会报错。
  2. 解构赋值不会拷贝继承自原型对象的属性(即不会继承来自__proto__的属性)。

配合解构赋值

解构赋值配合拓展运算符,还可以很方便地扩展某个函数的参数,引入其他操作。

function newFunction({ x, y, ...restConfig }) {
  // 使用x和y参数进行操作
  // 其余参数传给原始函数
  return originFunction(restConfig);
}

快速拷贝拓展对象

  1. 取出参数对象的所有可遍历属性,拷贝到当前对象之中。
let z = { a: 1, b: 2 };
let n = { ...z }; // n = { a: 1, b: 2 }
  1. 快速合并两个对象。
let ab = { ...a, ...b };

我们会发现,使用拓展运算符...进行对象的拷贝和合并,其实与ES6中另外一个语法糖Object.assign()效果一致:

// 上面的合并等同于
let ab = Object.assign({}, a, b);

需要注意的有:

  • 它们都只会拷贝源对象自身的并且可枚举的属性到目标对象身上
  • 它们都是浅拷贝,即对象数组等将拷贝引用值
  • 对多个对象进行拷贝时,相同的属性名,后面的将覆盖前面的

rest参数

ES6引入rest参数(形式为...rest),用于获取函数的多余参数,这样就不需要使用arguments对象了。
rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

function add(...values) {
  let sum = 0;
  values.forEach(x => {sum += x;})
  return sum;
}
add(1, 2, 3) // 6

替换arguments:

// arguments变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

同样要注意的是,rest只能是最后一个参数。

说到arguments,这里插播一下尾调用优化。

  • 尾递归

递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。
但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

  • 尾调用优化

ES6的尾调用优化只在严格模式下开启,正常模式是无效的。因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈:

  1. func.arguments:返回调用时函数的参数。
  2. func.caller:返回调用当前函数的那个函数。

一起来「拓展」

对象的拓展

对象拓展了一些很方便的属性,简化了我们很多的工作。常用的:

  • Object.assign()

用于将所有可枚举的属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

  • Object.keys()

返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。

听着好复杂,但是很多时候当我们需要遍历某个对象的时候就很方便了:

Object.keys(someObj).forEach((key, index) => {
    // 需要处理的操作
});
  • Object.values():与Object.keys()相似,返回参数对象属性的键值
  • Object.entries:同上,返回参数对象属性的键值对数组

数组的拓展

数组也拓展了一些属性:

  • Array.from():用于将两类对象转为真正的数组
  • Array.of():用于将一组值,转换为数组
  • 其它的entries()keys()values()

这里只介绍可能比较常用的:

  • Array.find():用于找出第一个符合条件的数组成员

参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined

[1, 4, -5, 10].find((n) => n < 0); // -5
  • Array.findIndex():用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

  • Array.includes():返回一个布尔值,表示某个数组是否包含给定的值

数据结构的拓展

  • Set

它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set本身是一个构造函数,用来生成Set数据结构。

从此我们的去重就可以这样写了:

let newArray = Array.from(new Set(oldArray));
  • Map

JavaScript的对象(Object),本质上是键值对的集合(Hash结构),但是传统上只能用字符串当作键。

// 不信你可以试试看
const obj = {a: 123};
const a = [];
a[obj] = 1;
console.log(a["[object Object]"]);

原因是对象只接受字符串作为键名,所以obj被自动转为字符串[object Object]

Map数据结构类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
也就是说,Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应,是一种更完善的Hash结构实现。


关于简写那些事

属性的简写

ES6允许直接写入变量和函数,作为对象的属性和方法。

// 属性简写
function f(x, y) {
  return {x, y}; 
  // 等同于
  return {x: x, y: y};
}

// 方法简写
var obj = {
  method() {}
  // 等同于
  method: function() {}
};

箭头函数

ES6允许使用“箭头”(=>)定义函数。

var f = () => 5;
// 等同于
var f = function () { return 5 };

箭头函数有几个使用注意点:

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
  2. 不可以当作构造函数,即不可以使用new命令。
  3. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。

最关键的是第一点:this对象的指向是可变的,但是在箭头函数中,它是固定的。

function normalFunction() {
  setTimeout(function(){
    console.log(this.name);
  }, 100);
}
function arrowFunction() {
  setTimeout(() => {
    console.log( this.name);
  }, 100);
}

var name = 'outer';

normalFunction.call({ name: 'inner' }); // 'outer'
arrowFunction.call({ name: 'inner' }); // 'inner'

结束语

这里我们介绍了ES6/ES7一些基础比较普遍的点,像解构、拓展表达式(...)、数组对象等拓展属性等等,基本上是一些提高开发效率,减少冗余重复的代码的新特性和新语法。
而像改变我们设计思维、甚至是解决方案的则是一些较复杂的,像ClassModulePromiseasync/await等等,咱们分篇讲,或者查ECMAScript 6 入门手册吧哈哈。

关于小程序 setData

你好。我先描述下背景吧。
我还没正经开发过小程序,最近在做相关工作(封装一个小程序框架)
比如有这样一个业务场景,一个页面里有一个组件,页面监听了某个子组件的事件,在回调里setData了,而这个子组件因为监听了生命周期或者UI事件也setData了,并 trigger 了页面监听的这个事件。我想了解一下这个过程到底是怎样的?

  1. 是否会引发两次渲染?
  2. trigger 是同步还是异步的?理论上同步的可能性比较大

不太清楚 setData 到 webview 那边的通信时间和内部放置的队列情况,不过应该是宏队列,但是浏览器本身也对宏队列做了优化(比如连续在 setTimeout 0 操作 dom,不论是重绘还是重排,浏览器也会有自己的一套 isNeedRender 的算法优化)

重新认识Angular

谈谈Angular

本文跟随着Angular的变迁聊聊这个框架,分享一些基础的介绍,以及个人的理解。
也用过其他框架,像React和Vue。
但与Angular结识较深,或许也是缘分吧。
image


内容概要

  • 数据绑定 (updated)
  • 模块化组织 (new)
  • 依赖注入
  • 路由和lazyload (new)
  • Rxjs (new)
  • 预编译AOT (new)
  • 拥抱变化,迎接未来

updated: 原有特性,有更新
new: 新增特性


数据绑定


常用模版绑定

<!--插值表达式( {{...}} )-->
<p>The sum of 1 + 1 is {{1 + 1}}</p>

<!--属性 (property) 绑定 ( [property] )-->
<img [src]="heroImageUrl">

<!--事件绑定 ( (event) )-->
<button (click)="onSave()">Save</button>

<!--双向绑定 ( [(...)] ) -->
<my-sizer [(size)]="fontSizePx"></my-sizer>

<!--双向绑定=属性绑定+事件绑定-->
<my-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></my-sizer>

常用模版引擎

1. String-based 模板技术

基于字符串的parse和compile过程

image

字符串模板强依赖于innerHTML(渲染), 因为它的输出物就是字符串。

2. Living templating 技术

基于字符串的parse和基于dom的compile过程

image

事实上,Living template的compile过程相对与Dom-based的模板技术更加纯粹, 因为它完全的依照AST生成,而不是在原Dom上的改写

首先我们使用一个内建DSL来解析模板字符串并输出AST。
结合特定的数据模型(在regularjs中,是一个裸数据), 模板引擎层级游历AST并递归生成Dom节点(不会涉及到innerHTML)。
与此同时,指令、事件和插值等binder也同时完成了绑定,使得最终产生的Dom是与Model相维系的,即是活动的。

3. Dom-based 模板技术

基于Dom的link或compile过程
image
Dom-based的模板技术事实上并没有完整的parse的过程(先抛开表达式不说),如果你需要从一段字符串创建出一个view,你必然通过innerHTML来获得初始Dom结构. 然后引擎会利用Dom
API(attributes, getAttribute, firstChild… etc)层级的从这个原始Dom的属性中提取指令、事件等信息,继而完成数据与View的绑定,使其”活动化”。

所以Dom-based的模板技术更像是一个数据与dom之间的“链接”和“改写”过程

以上内容参考:《一个对前端模板技术的全面总结》


数据更新Diff

框架的数据更新:

  • React => 虚拟DOM
  • Vue => getter/setter
  • Angular => 脏检查

React

使用虚拟DOM进行Diff。Virtual DOM本质上就是在JS和DOM之间做了一个缓存。

Virtual DOM 算法
1. 用JS对象模拟DOM树
用JavaScript对象结构表示DOM树的结构;然后用这个树构建一个真正的DOM树,插到文档当中。

2. 比较两棵虚拟DOM树的差异
当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,通过深度优先遍历两棵树,每层的节点进行对比,记录两棵树差异。

3. 把差异应用到真正的DOM树上
把 2 所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了。

分享文章:《深度剖析:如何实现一个 Virtual DOM 算法》

Vue

1. Vue1:使用getter/setter Proxy进行更新。
Vue使用的发布订阅模式,是点对点的绑定数据。
Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
这里在对数据进行赋值和读取的时候,都会进行Proxy,然后点对点更新数据。

2. Vue2:使用虚拟DOM进行Diff。
参考React的虚拟DOM。

Angular

  • 核心:使用脏检测(新/旧值比较)Diff

    • 当Model发生变化,会检测所有视图是否绑定了相关数据,再更改视图
  • Zone.js(猴子补丁:运行时动态替换)

    • 将Javascript中异步任务包裹一层,使其运行在Zone上下文中
    • 每一个异步任务为一个Task,提供钩子函数(hook)
  • Angular2+变化

    • zone.js对异步任务进行跟踪
    • 脏检查计算放进worker
    • Angular2+中树结构,自上而下进行脏检查(Angular1中的带有环的结构)

模块化组织

Angular模块

Angular模块把组件、指令和管道打包成内聚的功能块,每个模块聚焦于一个特性区域、业务领域、工作流或通用工具。

模块化**

功能模块抽象层层放射到整个应用程序。

根模块     
├── 登录模块      
├── 内部模块
│      ├ ··头部面包屑
│      ├ ··左侧菜单列
│      └── 展示内容
│            ├── 配置模块
│            ├── 展示模块
│            └── 日志模块
└── 公用模块

模块化**层层包裹,结构组织也层层地抽象封装,树结构的设计**从模块组织到依赖注入延伸。

模块修饰器

修饰器(Decorator)是一个函数,用来修改类的行为。

注意,修饰器(Decorator)并不是Typescript特性,而是ES6的特性。
ES2017引入了这项功能,目前Babel转码器已经支持。

@NgModule({ // NgModule修饰器
 	declarations: [
   		AppComponent,
   		NavComponent,
   		BreadcrumbComponent
 	], // 声明内部组件
 	imports: [
   		FormsModule,
   		HttpModule,
   		AppRoutingModule
 	], // 引入依赖模块
	exports: [], // 输出内部组件
 	providers: [ResultHandlerService], // 服务注入
 	bootstrap: [AppComponent]  // 启动应用
})

依赖注入

Angular的依赖注入可谓是灵魂了,之前有篇详细讲这个的文章《谈谈Angular2中的依赖注入》


什么是依赖注入

依赖注入在项目中,体现为项目提供了这样一个注入机制:
有人负责提供服务,有人负责消耗服务,而这样的机制提供了中间的接口,并替使用者进行了创建并初始化这样的处理。

我们只需要知道,拿到的是完整可用的服务就好了,至于这个服务内部的实现,甚至是它又依赖了怎样的其他服务,都不需要关注。

// 可注入的服务
@Injectable()
export class NameService {
    getName() { return Name; }
    changeName(name) { Name = name; }
}

依赖注入与状态管理

状态管理:

  • Angular:依赖注入服务来共享一些状态
  • 其他框架(React/Vue)的状态管理:组件传递、bus总线、事件传递、状态管理工具Redux/Flux/Vuex

其实像我们设计一个项目,自行封装的一些组件和服务,然后再对它们的新建和初始化等等,也经常需要用到依赖注入这种设计方式的。

我们的服务也可以分为有记忆的和无记忆的,关键在于抽象完的组件是否内部记录自身状态,以及怎样维护这个状态等等,甚至设计不合理的话,这样的状态管理会经常使我们感到困扰,所以Redux、Flux和Mobx这样的状态管理框架也就出现了。

而Angular在某种程度上替我们做了这样的工作,并提供我们使用。

在Angular里面我们常常通过服务来共享一些状态的,而这些管理状态和数据的服务,便是通过依赖注入的方式进行处理的。

依赖注入还有有个很棒的地方,就是单元测试很方便,测试的时候也注入需要的服务就好了。


多级依赖注入

多级依赖注入:组件树与注入器树平行。

一个Angular应用是一个组件树,同时每个组件实例都有自己的注入器,组件的树与注入器的树平行。

现在树结构已经在前端领域越来越流行了,浏览器的DOM树/CSS规则树、React的虚拟DOM、以及Angular(其实不只是Angular)的组件树和注入器树。

上面也说道,并不是所有的组件都会注入服务的,所以有了”注入器冒泡”:

当一个组件申请获得一个依赖时,Angular先尝试用该组件自己的注入器来满足它。如果该组件的注入器没有找到对应的提供商,它就把这个申请转给它父组件的注入器来处理。


路由和lazyload

image

像我们打包页面,很多时候最终生成了一个bundle.js文件。这样,每次当我们请求页面的时候,都请求整个bundle.js并加载,有了Webpack或许我们只需要加载其中的某些模块,但还是需要请求到所有的代码。
很多时候我们或许不需要进入所有的模块,这个时候浪费了很多的资源,同时首屏体验也受到了影响。

通过路由的lazyload以及上面提到的模块化,我们可以把每个lazyload的模块单独打包成一个分块bundle文件,当进入模块时才请求和加载,当我们的业务规模很大的时候,首屏速度得到大幅度提升。


Rxjs

很多时候我们都拿RxjsPromise来比较,但其实它们有很大的不一致。
以下很多内容来自《不要把Rx用成Promise》


核心**: 数据响应式

  • Promise => 允诺
  • Rxjs => 由订阅/发布模式引出来

image

  1. Promise顾名思义,提供的是一个允诺,这个允诺就是在调用then之后,它会在未来某个时间段把异步得到的result/error传给then里的函数。

  2. Rx不是允诺,它本质上还是由订阅/发布模式引出来的,它的核心**就是数据响应式,源头是数据产生者,经过一系列的变换/过滤/合并的操作,被数据消费者所使用,数据消费者何时响应,完全取决于数据流何时能流下来。


执行和响应

1. Promise需要then()catch()执行,并且是一次性的。

Promise需要调用then或者catch才能够执行,catch是另一种形式的then,调用then或者catch之后,它返回一个新的Promise,这样新的Promise也可以同样被调用,所以可以做成无限的then链。

2. Rxjs数据的流出不取决于是否subscribe(),并且可以多次响应。

Rx的数据是否流出不取决于是否subscribe,也就是说一个observable在未被订阅的时候也可以流出数据,在之后它被订阅过后,先前的数据是无法被数据消费者所查知,所以Rx还引入了一个lazy模式,允许数据缓存着直到被subscribe,但是数据是否流出还是并不依赖subscribe。

Rx的observable被subscribe之后,并不是继续返回一个新的observable,而是返回一个subscriber,这样用来取消订阅,但是这也导致了链式断裂,所以它不能像Promise那样组成无限then链。


数据源头和消费

1. Promise没有确切的数据消费者,每一个then都是数据消费者,同时也可能是数据源头,适合组装流程式(A拿到数据处理,完了给B,B完了把处理后的数据给C,以此类推)。

Promise的数据是一次性流出的,因为Promise内部维持着状态,初始化的pending,转成resolved或者rejected之后,状态就不可逆转了。

举例说promise().then(A).then(B).then(C).catch(D),数据是顺着链以此传播,但是只有一次,数据从A到B之后,A这个promise的状态发生了改变,从pedding转成了resolved,那么它就不可能再产生内容了,所以这个promise已经不是活动性的了。

2. Rxjs则有明确的数据源头,以及数据消费者。

Rx则不同,我们从Rx的接口就可以知道,它有onNext,onComplete和onError,onNext可以响应无数次,这也是符合我们对数据响应式的理解,数据在源头被隔三差五的发出,只要源头认为没有流尽(onComplete)或者出了问题(onError),那么数据就可以不断的流到响应者那边。


Rxjs例子

const observable
 = Rx.Observable.fromEvent(input, 'input')    // 监听 input 元素的 input 事件
     .map(e => e.target.value)                         // 一旦发生,把事件对象 e 映射成 input 元素的值
     .filter(value => value.length >= 1)             // 接着过滤掉值长度小于 1 的
     .distinctUntilChanged()                             // 如果该值和过去最新的值相等,则忽略
     .subscribe(                                                // subscribe 拿到数据
       x => console.log(x),
       err => console.error(err)
     );

// 订阅
observable.subscribe(x => console.log(x));

用AOT进行编译


JIT

JIT编译导致运行期间的性能损耗。由于需要在浏览器中执行这个编译过程,视图需要花更长时间才能渲染出来。

由于应用包含了Angular编译器以及大量实际上并不需要的库代码,所以文件体积也会更大。更大的应用需要更长的时间进行传输,加载也更慢。


AOT

预编译(AOT)会在构建时编译,这样可以在早期截获模板错误,提高应用性能。

AOT使得页面渲染更快,无需等待应用首次编译,以及减少体积,提早检测模板错误等等。


预编译(AOT) vs 即时编译(JIT)

只有一个Angular编译器,AOT和JIT之间的差别仅仅在于编译的时机和所用的工具。

使用AOT,编译器仅仅使用一组库在构建期间运行一次;
使用JIT,编译器在每个用户的每次运行期间都要用不同的库运行一次。


拥抱变化,迎接未来

身为框架,Angular和React、Vue各有各的优劣,哪个更适合则跟产品设计、应用场景以及团队等各种因素密切相关,没有谁是最好的,只有当前最适合的一个。

Angular的经常性不兼容、断崖式升级,或许对开发者不大友好。
但它对新事物的接纳吸收、勇于颠覆自身,是面向未来开发的好榜样。

我们也何尝不是,为何要死守某个框架、某种语言,或是争好坏、分高下。
何尝不抱着开放的心态,拥抱变化,然后迎接未来呢?


结束语

以上只是本人的个人理解,或许存在偏差。世上本就没有十全十美的事物,大家都在努力地相互宽容和理解。
那些我们想要分享的东西,肯定是存在很棒的亮点。而我们要做的,是尽力把自己看到的那完美的一面呈现给大家。
与其进行口水之争,取精辟,去糟粕,不更是面向未来的方式吗?

参考

一个组件的自我修养

曾经我在面试的时候,面试官问我,觉得做过的有意思的东西是什么,答组件相关的。结果被面试官鄙视了,sign~不过呢,再小的设计,当你把满腔热情和各种想法放进里面,它似乎有了灵魂。或许是我当时面试的表达,没有传达到真正的想法,那么在这里,我希望能很好地表达。

组件的划分

前面我们简单说明了下组件的封装和划分,参考《页面区块化与应用组件化》

通过视觉和交互划分

通常来说,组件的划分,与视觉、交互等密切相关,我们可通过功能、独立性来判断是否适合作为一个组件。

这次我们拿知乎的内容卡片来说吧,上图:

image

可以看到,这里我们每个卡片,内容都稍微有些不一样。但毫无疑问,它们拥有相同的功能,可通过一个组件来控制内容的展示。

那么我们大概可以这样来表示这个组件(为了方便,该文章大部分代码基于 Vue 来表示吧):

<my-card :innerData="eachCardData" :cardType="'videoCard' | 'photoCard' | 'textCard'"></my-card>

其中,innerData 传入卡片内容,包括标题、文字、图片、附加信息(点赞数、评论数、日期等)。
同时,我们可以通过 cardType 来告诉组件,这是个视频类型、图片类型、还是纯文字类型的内容,来让组件控制内容和样式展示。

通过代码复用划分

我们在写代码的时候,观察到一些代码,他们在结构和功能上其实是可复用的,这个时候我们也可以通过封装的办法,把它们封装一起,以减少重复的代码。

同样的,我们拿右侧的一个快捷导航模块来看:

image

一般来说,从功能划分的话,我们会把外面那层封装成一整个组件:

<quick-link-panel>
    <div v-for="item in quickLinkItem">...</div>
</quick-link-panel>

这时候有人跑出来说,每一行我们都可以视为独立的一个组件,看:

<quick-link-line link="where/to/go" :text="lineText" :tagNum="numberWithTag"></quick-link-line>

Emmmmmmm。。好像这样也看不出来区别把?似乎也没有简化到什么代码?

不!对方说,在这样的模块里面就可以快速使用了:

<quick-link-panel-with-text>
    <h1>我可是有标题的噢</h1>
    <quick-link-line v-for="item in quickLinkItem" :link="item.link" :text="item.lineText" :tagNum="item.numberWithTag"></quick-link-line>
</quick-link-panel-with-text>>

=.=好吧,可能这里举得例子不够特别鲜明。不过大抵意思是这样啦。

但其实这不是很好运用的一种方式,因为控制不好的话,可能你的代码会过度封装,导致别人在维护的时候,表示:卧槽!!!这得跳多少层才能找到想看的代码!!!

组件的封装

怎样才能算是一个合格的组件呢?我们在设计的时候,经常要考虑解耦,但很多时候,过度的解耦反而会导致项目复杂度变高,维护性降低。

独立的组件

组件的独立性,可以包括以下几个方面:

  • 维护自身的数据和状态、作用域
  • 维护自身的事件

同样拿之前的内容卡片来看:

image

这是个独立的卡片:

  1. 它拥有自己的数据,包括标题、文字、图片、点赞数、评论数、日期等。
  2. 它拥有自身的状态,是否已点赞、是否已收藏、是否详细展示内容、是否展示评论等。
  3. 它维护着自己的事件,包括点击分享、收藏、点赞、回复等等。
<template>
    <div>
        <h2>{{model.question}}</h2>
        <div :class="isContextShown ? 'content-detail' : 'content-brief'">
            <div v-if="model.withImage"><img :url="model.imageUrl" /></div>
            <div>{{model.content}}</div>
        </div>
        <div>
            <span @click="likeIt()">点赞</span>
            <span @click="keepIt()">收藏</span>
        </div>
    </div>
</template>
<script>
export default {
  name: 'my-card',
  data() {
    return {
      model: {},
      isContextShown: false
    };
  },
  methods: {
    likeIt() {},
    keepIt() {}
  },
  mounted() {}
};
</script>

嗯,去掉很多功能之后,大概是这么简单的一个组件吧[捂脸]。

组件与外界

我们在保持组件独立性的时候,当然还需要考虑它与外界的交互,主要包括:

对外提供配置项,来控制展示以及具体功能。

这里最简单的,我们每个卡片都需要传入内容,我们一次性拉取列表数据,并传入每个卡片,在 Vue 中可以使用 props。

对外提供查询接口,可从外界获取组件状态。

这个的话,更多时候我们是通过事件等方式来告诉外界一些事情。在这里举个例子,我们这里假设一个页面只允许一个卡片内容处于详细展开状态,故我们需要获取其展开的操作,方便控制。

<template>
    <div>
        <h2>{{model.question}}</h2>
        <div @click="toggleContext()" :class="isContextShown ? 'content-detail' : 'content-brief'">
            <div v-if="model.withImage"><img :url="model.imageUrl" /></div>
            <div>{{model.content}}</div>
        </div>
        <div>
            <span @click="likeIt()">点赞</span>
            <span @click="keepIt()">收藏</span>
        </div>
    </div>
</template>
<script>
export default {
  name: 'my-card',
  props: { // 传入数据
    model: {
      type: Object,
      default: () => {}
    }
  },
  data() {
    return {
      isContextShown: false
    };
  },
  methods: {
    likeIt() {},
    keepIt() {},
    toggleContext() {
        // 点击展开/收起的时候通知
        this.isContextShown = !this.isContextShown;
        this.$emit('toggle', this.isContextShown);
    }
  },
  mounted() {}
};
</script>

简单调整之后,我们会这样使用:

<my-card :model="cardModel" @toggle="doSomething()"></my-card>

这是最简单的对内和对外的联系,对一个组件来说,它也有 in 和 out 两个方向的流动。

在 Vue 里,如果父组件需要获取子组件的实例,也可以通过通过vm.$refs来获取。

结束语

这里主要从单个组件的角度来进行说明,搭配一点点的代码,防止文字太多难以理解。
其实组件的封装,与我们很相似。我们需要拥有独立的空间,但不能完全封闭,我们需要从其他地方获取能量,同时也需要反馈给其他人,来完成更多的协助与配合。

前端思维转变--从事件驱动到数据驱动

接触过jQuery的小伙伴们大概在切换到mvvm初总不习惯,需要进行开发思维的转换,从事件驱动的角度出发,到从数据驱动的角度出发,也是不小的挑战。

事件驱动

GUI与事件

GUI(图形用户界面)与事件驱动的渊源可谓不浅。

GUI应用程序的特点是注重与用户的交互,因此程序的执行取决于与用户的实时交互情况,大部分的程序执行需要等到用户的交互动作发生之后。

由于用户的输入频率并不高,若不停轮询获取用户输入,就有点像ajax轮询和websocket推送的关系:

  1. 资源利用率低。
  2. 不能真正做到及时同步。

由于GUI程序的执行流程由用户控制,并且不可预期,为了适应这种特点,我们需要采用事件驱动的编程方法。普通程序的执行可概括为“启动——做事——终止”,而事件驱动的程序的执行可概括为“启动——事件循环(即等待事件发生并处理之)”。

事件驱动编程

事件

事件是可以被控件识别的操作,如按下确定按钮,选择某个单选按钮或者复选框。每一种控件有自己可以识别的事件,如窗体的加载、单击、双击等事件,编辑框(文本框)的文本改变事件,等等。

事件(event)是针对应用程序所发生的事情,并且应用程序需要对这种事情做出响应。

事件处理

程序对事件的响应其实就是调用预先编制好的代码来对事件进行处理,这种代码称为事件处理程序(event handler)。

事件驱动编程(event-driven programming)就是针对这种“程序的执行由事件决定”的应用的一种编程范型。

Event loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

关于Javascript的单线程与Event Loop,想要了解可以参考《JavaScript 运行机制详解:再谈Event Loop》。今天的主角是数据驱动,事件相关的不进行详细说明了。

事件驱动思维

在GUI和Javascript的设计场景下,我们写代码的时候也会代入这样的思维:

用户输入 => 事件响应 => 代码运行 => 刷新页面状态

于是乎,刚开始写应用的思路如下:

  1. 开发静态页面。
  2. 添加事件监听,包括用户输入、http请求、定时器触发等事件。
  3. 针对不同事件,编写不同的处理逻辑,包括获取事件状态/输入、计算并更新状态等。
  4. 根据计算后的数据状态,重新渲染页面。

通俗地说,事件驱动思维是从事件响应出发,来完成应用的设计和编程。

数据驱动

数据驱动,将我们从复杂的逻辑设计带进数据处理的世界。

何为数据

数据是什么,官方回答:数据是科学实验、检验、统计等所获得的和用于科学研究、技术设计、查证、决策等的数值。

但其实不管是资料中、生活和工作中,所有的事物我们都可以抽象为数据。像游戏里面的角色、物品、经验值、天气、时间等等,都是数据。游戏其实也算是对真实世界抽象的一种,而抽象之后,最终都可呈现为数据。

我认为,数据是一个抽象的过程。

回到日常写码中,前端写页面,抽象成数据常用的无非是:

  • 列表 => array
  • 状态 => number/boolen
  • 一个卡片 => object
  • 等等

事件驱动到数据驱动

数据驱动 vs 事件驱动

要对事件驱动和数据驱动进行直观的比较,大概是以下这样:

事件驱动

  1. 构建页面:设计DOM => 生成DOM => 绑定事件
  2. 监听事件:操作UI => 触发事件 => 响应处理 => 更新UI

数据驱动

  1. 构建页面:设计数据结构 => 事件绑定逻辑 => 生成DOM
  2. 监听事件:操作UI => 触发事件 => 响应处理 => 更新数据 => 更新UI

其实最大的转变是,以前会把组件视为DOM,把事件/逻辑处理视为Javascript,把样式视为CSS。而当转换思维方式之后,组件、事件、逻辑处理、样式都是一份数据,我们只需要把数据的状态和转换设计好,剩下的实现则由具现方式(模版引擎、事件机制等)来实现。

数据驱动思维

转换到数据驱动思维后,我们在编程实现的过程中,更多的是思考数据的维护和处理,而无需过于考虑UI的变化和事件的监听。

拿一个企业网站来说,里面的很多数据和链接,从前我们常用方式是直接写成DOM,然后就产生了很长的一段DOM代码。

如果说我们将其切换到数据,以对象和数组的方式存储,这时候我们只需要写一段具现方式,将这组数据转成DOM。这种方式有以下好处:

  • 数据变更方便
  • DOM结构变轻
  • DOM结构/样式调整方便
  • 抽象设计
  • 代码量减少,易于维护

数据驱动与mvvm

数据驱动的设计思维或许与mvvm没有必然的联系,但是mvvm框架提供一些具现方式将数据驱动变得更加轻松。

mvvm集成具现化方法

拿vue框架来说,有以下一些很方便的具现方法:

  • 模板渲染:数据 => AST => 生成DOM
  • 数据绑定:交互输入/http请求响应/定时器触发 => 事件监听 => 数据变更 => diff => DOM更新
  • 路由引擎:url => 数据(host/path/params等) => 解析对应页面

当我们使用了这些mvvm框架时,它们解决了如何让数据转变成需要的东西,将抽象具象化的问题。在这样的情况下,我们只需要完成两步:

  1. 将产品/业务/设计抽象化,将UI、交互抽象为数据。
  2. 将一组组的数据用逻辑处理连接起来。

mvvm推动数据驱动思维

这里借用vue,来举两个例子吧。

一、获取input输入并更新
实现一个input的监听输入,并更新输出到模板,我们能有以下代码的变化:

<!--1. 事件驱动-->
<input type="text" id="input" />
<p id="p"></p>
<script>
$('#input').on('click', e => {
    const val = e.target.value;
    $('#p').text(val);
})
</script>

<!--2. 数据驱动 + vue-->
<input type="text" v-model="inputValue" />
<p>{{ inputValue }}</p>

当我们在vue中,模板引擎帮我们处理了模板渲染、数据绑定的过程,我们只需要知道这里面只有一个有效数据,即input的值。

二、部分更新列表
我们再来看个例子,我们有一组数据,需要渲染成一个列表:

const list = [
    {id: 1, name: 'name1', href: 'http://href1'},
    {id: 2, name: 'name2', href: 'http://href2'},
    {id: 3, name: 'name3', href: 'http://href3'},
    {id: 4, name: 'name4', href: 'http://href4'}
]
  1. 当我们需要渲染成列表时:
<!--1). 事件驱动-->
<ul id="ul"></ul>
<script>
const dom = $('#ul');
list.forEach(item => {
    dom.append(`<li data-id="${item.id}"><span>${item.name}</span>: <a href="${item.href}">${item.href}</a></li>`)
});
</script>

<!--2). 数据驱动 + vue-->
<ul>
    <li v-for="item in list" :key="item.id"><span>{{item.name}}</span><a :href="item.href">{{item.href}}</a></li>
</ul>
  1. 当我们需要更新一个列表中某个id的其中一个数据时(这里需要更改id为3的name值):
// 1). 事件驱动
const dom = $('#ul');
const id = 3;
dom.find(`li[data-id="${id}"] span`).text('newName3');

// 2). 数据驱动 + vue
const id = 3;
list.find(item => item.id == 3).name == 'newName3';

当然这里我们已知list里面有id为3的值,若是未知或不确定的数据,则需要做好异常处理,如:

const id = 3;
const item3 = list.find(item => item.id == 3);
if(item3) item3.name == 'newName3';

在使用数据驱动的时候,模板渲染的事情会交给框架去完成,我们需要做的就是数据处理而已。

结束语

思维的切换和视角的转变,是一件很有意思的事情。从更多的角度去观察,去思考,去总结,才能更好地理解被观察体。

爱了爱了

Offer my love star❤
截屏2020-06-02 下午8 31 09
This is my goddess, so amazing!

组件配置化

配置化**,其实可以在很多的地方使用。很多时候,我们在设计接口、应用、数据等情况下,配置化的方式可以允许我们获得更高的自由度。这里我们简单讲讲组件的配置化吧。

配置化**

可配置的数据

数据的配置,或许大家会比较熟悉。像一些静态数据呀,或者说我们很多的管理端都是用来进行数据配置的。

应用中的可配置数据

最常见的数据配置,大概是应用里面的配置,文案呀、说明等,为此我们有了运营这样的职位。常见的方式,则是搭起一整套的运营管理平台,一些简单的文字或是数据,则可以通过平台进行配置。

代码中的可配置数据

有些时候,我们也会在代码里面放置一些可配置的数据。例如说,这个需求需要查询一周的数据,常见的做法是将天数配置为7天:

const QUERY_DAY_NUM = 7;

这样,当需要在紧急情况支持其他天数(五一、国庆、过年能假期)的时候,我们就可以只需要改动这里就好啦。

文件里的可配置数据

有些情况下,一些影响逻辑的配置数据,要是直接写到代码里,在调整的时候通常需要重新打包部署,这样情况下开销大、效率低。

所以在一些时候,我们会把这样的可配置数据,单独写到某个文件里维护。当需要调整的时候,只需要下发一个配置文件就好啦。

可配置的接口

关于接口的配置化,目前来说见过的不是特别多。毕竟目前来说,我们的很多数据和接口并不是简单的增删查改这样的功能,很多时候还需要在接口返回前后,做一系列的逻辑处理。

简单地说,很多的业务接口场景复用性不高,前后端除去协议、基础规范的定义之后,很少再能进行更深层次的抽象,导致接口配置化的改造成本较大。

配置化的实现有两点很重要的东西:规范解决方案。如果说目前较好的从前端到后台的规范,可能 GrapgQL 和 Restful ,大家不熟悉的也可以去看看啦。

当然,或许有些团队已经实现了,也希望尽快能看到一些相关的解决方案啦。

可配置的页面

页面的配置化,可能也已经不少见了吧。像我刚出道的时候,也写过一个拖拽的 demo,虽然现在光想想那时候的 jQuery 代码就惨不忍睹,不过当时还是觉得自己挺牛逼的。

有些时候,一些页面比较简单,里面的板块、功能比较相似,可能文案不一致、模块位置调整了、颜色改变等等。虽然说复制粘贴再改一改,很多时候也能满足要求,但是配置化的**,就是要把重复性的工作交给机器呀。

这种页面的配置,基本上有两种实现方式:

  1. 配置后生成静态页面的代码,直接加载生成的页面代码。
  2. 写通用的配置化逻辑,在加载页面的时候拉取配置数据,动态生成页面。

基于 SEO 和实现复杂度各种情况,第一种方式大概是目前比较常用的,第二种的实现难度会稍微大一些,同时对于 SEO 与单页应用有着相同的困境。

第一种方式,很多适用于一些移动端的模版页面开发,例如简单的活动页面、商城页面等等。

第二种的话,更多的是一些管理平台的实现,毕竟大多数都是增删查改,形式无非列表、表单和菜单等。之前我也捣鼓过一小会的 Angular 表单、列表等配置,可以看看Angular自定义页面和这个Angular2 Schema Form Exemple。嗯,可能有些bug,don't mind don't mind~

可配置的应用

可配置的应用,大概更多地是从业务和应用设计的角度出发。当我们设计一个应用的时候,页面是活动的、可变的、可配置的,这些都是需要考虑到的地方。

当然,如果说涉及到代码实现的话,那大概与上面的相关。配置化的**,其实或许不局限于代码、工程和我们的工作,甚至我们完全可以拓展至我们的生活中。

组件配置化

那么这里我们来讲一下简单的配置化组件的实现把。关于组件的封装,我们在《一个组件的自我修养》一文也讲述过。

下面的组件,我们同样拿这样一个卡片组件来作为例子吧。

image

可配置的数据

首先是数据的配置,这大概是最基础的,当我们在封装组件的时候,很多数据都是通过作用域内的变量来动态绑定的,例如 Vue 里面则是datapropscomputed等维护 scope 内的数据绑定。

作为一个卡片,内容是从外面注入的,所以我们可以通过 props 来获取:

<template>
    <div>
        <h2>{{model.question}}</h2>
        <div>
            <div v-if="model.withImage"><img :url="model.imageUrl" /></div>
            <div>{{model.content}}</div>
        </div>
        <div>
            <span @click="likeIt()">点赞</span>
            <span @click="keepIt()">收藏</span>
        </div>
        <div>
          <p v-for="comment in model.comments">{{comment}}</p>
        </div>
    </div>
</template>
<script>
export default {
  name: 'my-card',
  props: { // 传入数据
    model: {
      type: Object,
      default: () => {}
    }
  },
  data() {
    return {
      isContextShown: false
    };
  },
  methods: {
    likeIt() {},
    keepIt() {}
  },
  mounted() {}
};
</script>

这里只展示简单的方式,我们会这样使用:

<my-card :model="cardModel"></my-card>

可配置的样式

样式的配置,通常是通过class来实现的。其实这更多地是对样式的配置化设计,与我们的 HTML 和 Javascript 关系则比较少。

样式的配置,需要我们考虑 CSS 的设计,通常来说我们有两种方式:

  1. 根据子元素匹配,来描述 CSS。
  2. 根据子 class 匹配,来描述 CSS。

根据子元素配置CSS

这是以前比较常用的一种方式,简单地说,就是通过 CSS 匹配规则中的父子元素匹配,来完成我们的样式设计。

例如,我们有个模块:

<div class="my-dialog">
  <header>I am header.</header>
  <section>
    blablablabla...
  </section>
  <footer>
    <button>Submit</button>
  </footer>
</div>

样式则会这样设计:

.my-dialog {
  background: white;
}
.my-dialog > header {}
.my-dialog > section {}
.my-dialog > footer {}

或者说用 LESS 或是 SASS:

.my-dialog {
  background: white;
  > header {}
  > section {}
  > footer {}
}

通过这种方式设计,或许我们在写代码的时候会稍微方便些,但是在维护上面很容易踩坑。曾经我也喜欢这种方式写,后来在一次次的 DOM 结构调整过程中,差点掉坑里出不来,于是乎后面慢慢改用第二种方式。

根据子 class 配置CSS

其实相对于匹配简单的父子和后代元素关系,使用 class 来辅助匹配,则可以解决 DOM 调整的时候带来的问题。

这里我们使用 BEM 作为例子来解释下大概的想法吧:

BEM
BEM的意思就是块(block)、元素(element)、修饰符(modifier),是一种前端命名方法论。更多的大家可以去谷歌或者百度下。

简单说,我们写 CSS 的时候就是这样的:

.block{}
.block__element{}
.block--modifier{}
  • block:可以与组件和模块对应的命名,如 card、dialog 等
  • element:元素,如 header、footer 等
  • modifier:修饰符,可视作状态等描述,如 actived、closed 等

这样的话,我们上述的代码则会变成:

<div class="my-dialog">
  <header class="my-dialog__header">I am header.</header>
  <section class="my-dialog__section">
    blablablabla...
  </section>
  <footer class="my-dialog__footer">
    <button class="my-dialog__btn--inactived">Submit</button>
  </footer>
</div>

搭配 LESS 的话,其实样式还是挺好写的:

.my-dialog {
  background: white;
  &__header {}
  &__section {}
  &__footer {}
  &__btn {
    &--inactived
  }
}

Emmmmmm。。其实大家看了下,就发现这样的弊端了,这样我们在写 HTML 的时候,需要耗费很多的时间来写这些 class 名字,更惨的是,当我们需要切换某个元素状态的时候,判断条件会变得很长,像:

<button :class="isActived ? 'my-dialog__btn--actived' : 'my-dialog__btn--inactived'">Submit</button>

简直惨不忍睹。当然我们也可以把修饰符部分脱离,这样使用:

<button class="my-dialog__btn" :class="isActived ? 'actived' : 'inactived'">Submit</button>

这样会稍微好一些。BEM 的优势和弊端也都是很明显的,大家也可以根据具体的团队规模、项目规模、使用场景等,来决定要怎么设计。

当然,如今很多框架都支持样式的作用域,通常是通过在 class 里添加随机MD5等,来保持局部作用域的 class 样式。常见的话,我们是搭配第一和第二种方式一起使用的。

可配置的展示

可配置的展示,更多时候是指某些模块是否展示、展示的样式又是如何等。

例如,我们需要一个对话框,其头部、正文文字、底部按钮等功能都可支持配置:

<div class="my-dialog" :class="{'show': isShown}">
  <header v-if="model.title">{{model.title}}</header>
  <section v-if="model.content">{{model.content}}</section>
  <footer>
    <button v-for="button in model.buttons">{{button.text}}</button>
  </footer>
</div>

我们可以通过model.title来控制是否展示头部,可以通过model.buttons来控制底部按钮的数量和文字。

这只是最简单的实例,我们甚至可以通过配置,来控制出完全不一样的展示效果。搭配样式的配置,更是能让组件出神入化。当然,很多时候我们组件的封装是需要与业务设计相关,这样维护性能也会稍微好一些,这些前面也都有说到过。

可配置的功能

功能的配置,其实很多也与展示的配置相关。但是我们有些与业务相关的功能,则可以结合展示、功能来定义这样的配置。

举个例子,我们的这个卡片可以是视频、图片、文字的卡片:

  • 视频:点击播放
  • 图片:点击新窗口查看
  • 文字:点击无效果

这种时候,我们可以两种方式:

  1. 每个功能模块自己控制,同时通过配置控制哪个功能模块的展示。
  2. 模块展示会有些耦合,但在点击事件里,根据配置来进行不同的事件处理,获取不同的效果。

对应维护性和可读性来说,第一种方式会获得更好的效果。如果问什么情况下会用到第二种,唔。。。大概是同样的呈现效果,在不同场景下的逻辑功能不一样时,使用比较方便吧。

功能配置化这块就不过多描述啦,毕竟这块需要与业务场景密切结合,大家更多地可以思考下,自己的项目中,是否可以有调整的空间,来使得整体的项目更好维护呢?

结束语

我们讲述了很多的配置化场景,也针对组件来详细描述了一些配置化的方向和方式。随着科技越来越发达,很多简单和重复性的事情我们可以交给机器去做,这就需要我们把相似的部分提取出来抽象封装,把可变的部分结合配置来高效地调整。
抽象封装和配置化的搭配,其实能获得很不错的效果,我们在对一些事物的认知上,也能进行更深层次的概括和思考。

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.