大家好又见面了
,过去几个月我们探讨了JAVA的方方面面,今天我们终于把目光转向了前端,带来前端技术的第一篇文章,讨论前端开发的工程化问题
。
前言
前端的发展虽说历史不如后端时间更长,但也经历了很多年一代代程序员的努力。预计当前入行的很多小伙伴并不了解前端技术是如何发展到今天的,我们先来通俗的讲一下历史。
1 从前后不分离时期,后端MVC的V层,到画页面,切图仔
我们绝大多数程序员可能认为,BS架构的网站起始于JSP。这种观点某种意义上没有太大问题。但是网页开发与前端技术的发起,要远远早于这个时期。Macromedia,估计没有多少人听过这个公司,但当时的“网页三剑客”可是顶顶大名。

特别幸运的是我在97年,十几岁的时候就已经接触到了电脑,也开心了买了一本在家里那台586上捣鼓过。Dreamweaver,Fireworks,Flash三剑客陪伴了我很长时间,当时甚至能在电脑上写出网页来掉起本地应用程序。可惜后来Macromedia公司兵败如山倒,2005年被Adobe公司收购,一代传奇惨淡收场。这三剑客现在网上依旧能查到,不过都变成了Adobe家的宝宝。这段时间属于静态独霸天下的太古时代。
接下来的故事大家应该都听过,程序猿们不满足与静态页面提供的简单逻辑,开始要求在页面端开发动态代码,于是JAVA强势介入,JSP横空出世。那时候,我们的程序猿大虾们是不分前后端开发的,好像几乎人人都是全栈工程师。页面怎么写?JSP就完事了。JSP在2000年后的第一个十年非常流行,还经历了1.2,2.0多个版本,也衍生出SimpleTag,FreeMarker等技术。但总的来说这段时期属于前后不分离的远古时代。
随着技术的更迭,业务场景的增加,前后端不分离的软件开发模式慢慢演变成界限清晰,分工精细的前后分离模式。这时的前端岗输出的工作,更多的是为后台MVC架构层面的View层,以html和css为主,不涉及业务逻辑。“切图仔”,“画页面的”也就成了当时前端的称呼代号。
以下代码是典型的示例:
2 Ajax来了
Ajax 即“
Asynchronous Javascript And XML”(异步 JavaScript 和 XML),是指一种创建交互式网页应用的网页开发技术。
- Ajax = 异步 JavaScript 和 XML 或者是 HTML(标准通用标记语言的子集)。
- Ajax 是一种用于创建快速动态网页的技术。
- Ajax 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。
不得不说,确实是Ajax改变了B/S开发的格局。在Ajax出现后,前端才渐渐成为了一个独立的工种。
function ajax(url,fn){ //创建xhr对象 var xhr = new XMLHttpRequest() //设置发送的服务器地址和方法 xhr.open("GET",url) //发送 xhr.send() //xhr状态改变的事件进行监听 xhr.onreadystatechange = function(){ console.log("readyState:"+xhr.readyState) console.log("status"+xhr.status) if(xhr.readyState==4&&xhr.status==200){ fn(xhr) } } }
上面的代码不清楚是否还有小伙伴记得
,估计这老古董大家都没看过了。其实就是js原生,没用任何框架写的Ajax原生代码。重点就是:
建立xmlHttpRequest对象
设置回调函数
使用OPEN方法与服务器建立连接
向服务器端发送数据
在回调函数中针对不同的响应状态进行处理
这5个步骤。那么Ajax最关键的作用,是让页面有了部分刷新的功能,避免了每次后台请求都需要刷新页面的差体验,同时也让前端页面能够通过js构写复杂的页面数据逻辑。
随着Ajax技术的越来越普及,很多基于这项技术为核心功能的前端js框架也纷纷出世。这些框架基本上实际上是分为两个大类:
- 功能强大,从界面到功能具有完整解决方案的大型框架,典型例子:早期的Ext JS,现在的Sencha
体积很小,但适用性很强的轻量级框架,典型例子:jquery
当然现在看来,jquery的统治力要远远高于其他的框架,统治了前端领域好多年。当然在这个过程中也出现了adobe flex的搅局,微软microsoft silverLight的试水,但最终软件开发的领域,个人的理解还是免费大于收费,开源大于产品。直到移动化浪潮的来临,jquery对于移动端支持和移动端体验的保障慢慢出现了疲态,虽然也推出了jquerymobile,但对响应式布局的支持确实不够看。于是后来又有了Bootstrap等新生的支持两端的框架,但真正的新变化,还是来自移动端的发展。
3 前端三大框架与node的出现
时间推进到2015年以后,移动开发已慢慢变为整个互联网开发的核心,随着整体开发工作领域的细分,前端的工作也越来越重要。
新一代的前端三大框架听说过吗?什么,没有?那你就out了
当前,三大主流前端框架分别是React、Vue、Angular这三个框架。
- React起源于Facebook的内部项目,用来架设Instagram的网站,于2013年5月开源。
- Vue是尤雨溪编写的一个构建数据驱动的Web界面的库,准确来说不是一个框架,它聚焦在V(view)视图层。
- Angular是一款优秀的前端JS框架,google出品。已经被用于Google的多款产品当中。
那么为啥我们原先已经有了jquery,为啥还要新框架?


根本的原因可以说就是以上两个图,我们前端的开发思路从上图的MVC模式,转化为了下图的MVVM模式。
何为MVVM模式?MVVM最早由微软提出来,它借鉴了桌面应用程序的MVC思想,在前端页面中,把Model用纯JavaScript对象表示,View负责显示,两者做到了最大限度的分离。也就是我们常说的,前后分离,真正在这里得以实现。所以前后分离,先做到的其实是前端的自我分离。
有神马不一样?
它采用双向绑定(data-binding):View的变动,自动反映在ViewModel,反之亦然,model数据的变动,也自动展示给页面显示。把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。
而我们上文提到的三大前端框架则均为MVVM的落地。新入坑的前端同学们可要好好的掌握这些框架哦

。
最后说说我觉得走的最远的,还是非node.js莫属了。node是第一个将js的手伸到服务器端的框架。
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。
Node.js能让一个前端工程师变成全栈工程师

。
我们会在后续章节里介绍node的其他细节。规范
说完前端发展简史,下面我们开始我们的重点内容,前端工程化实践。
工欲善其事必先利其器,coding 未起,规范先行。
1. 样式还原
页面的还原,尤其是在移动端上,不同屏幕尺寸下,如何做适配,我们的原则是,基准机型下100%还原,其他机型做相对比例适配(pc 端,基于设计稿px 值对应还原)。
有些同学,被问及移动端如何适配,大多能答上来用rem, 再细问的话,就说不出一二,这里就解释下,rem 是html 根目录字体大小, 例html标签的字体大小为100px,则1rem==100px, 当前页面下所有涉及到rem 的长度,都是这个关系,说回适配,通过获取屏幕的宽度,然后按比例动态设定html 的 font-size, 此时的rem 即是适配的, 对应的样式也就适配了,代码如下:
/** * rem 动态设置函数 * @param {*} pxToRem 设置px转换rem 比例,默认100 * @param {*} baseWidth 设置基准设计稿宽, 默认750px 基准机型iPhone6/7/8 * @returns */function htmlSizeCalculation(pxToRem, baseWidth) { pxToRem = pxToRem || 100; baseWidth = baseWidth || 750; // 获取根节点元素 var docEl = document.documentElement; // 屏幕变化监听event var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize'; // 核心方法,计算关系,并赋值根节点font size var recalc = function () { // 获取当前设备屏幕宽 var clientWidth = docEl.clientWidth; if (!clientWidth) return; // 根据当前设备屏幕宽,计算相应的根节点字体大小 docEl.style.fontSize = pxToRem * (clientWidth / baseWidth) + 'px'; }; recalc(); if (!document.addEventListener) return; // 屏幕变化,重新计算 window.addEventListener(resizeEvt, recalc, false); // 文档加载后,执行计算 document.addEventListener('DOMContentLoaded', recalc, false);}
2. 内容逻辑还原
严格按照需求文档,做逻辑还原。按需求行事,本来讲没什么需要强调的,但工作中发现,有些伙伴喜欢自己加成,自行添加页面上的文字话术,更有甚交付的成品与最终的需求南辕北辙,这些问题到测试环节,都会按bug处理。当然,也有可能是需求文档本身的纰漏,如有发现,需及时与需求人员沟通调整。
以上,不管是样式层面还是逻辑层面,我们坚持的原则一言以蔽之,尊重上游环节和专业人员的工作。
3. 命名规范
讲求见名知意,通常约定英文语义化命名,多单词可用中横线、小驼峰、大驼峰等格式,这个可以根据团队习惯,统一约定。语义化命名,对代码可读性的保障,笔者发现工作中,有小伙伴命名变量,百思不得意,有甚至反意命名,混淆逻辑,造成后期难以维护、项目交接困难等一系列问题。
例:
样式 中横线 some-name
变量/方法 小驼峰someName
图片名 下划线 some_name.png
常量 全大写下划线 SOME_NAME
组件名 大驼峰 SomeName
4. 代码检测
代码检测,通过一定的工具对项目代码做书写规范、冗余等方面检测,提示开发者对应代码行位置存在的 error、warning,以便修正;检测的目的是,一方面规范化的代码对其他开发者友好的可读性、可维护性,二是可以加强代码质量、规避潜在风险;工具有多种,比较常用的eslint,我们一般选用其standard规范,并稍加修改,团队也可以根据内部习惯,选择合适的工具。各家IDE也有一些相关格式化的工具,方便开发者做代码书写规范的格式化 。如有团队人员对此意识不足,可以使用git hook 进行提交分支时代码检测,有任何error则阻塞提交。
eslint可以在package.json、 .eslintrc 和 webpack中进行配置,具体可参考官网https://eslint.org/docs/developer-guide/nodejs-api#cliengine,以下是eslint 在webpack.config.js中的配置示例。
{ test: /\.js$/, exclude: /node_modules/, loader: 'eslint-loader', options: { // community formatter formatter: require('eslint-friendly-formatter'), // This option will enable ESLint autofix feature. fix: true, // This option will enable caching of the linting results into a file. cache: true, }, },
5. 分支规范
约定分支的意义,是保证关键节点的快速相应,生产的稳定性,开发的灵活性;我们选用git做版本控制,在分支规范上,master 稳定生产版本,develop 为主开发分支,feature/xxxx 功能模块的分支,hotfix紧急修复分支。
master分支,是从其他分支合并,存放的是供生产部署的稳定代码,每次合并是一次版本更迭,可打标签tag。
develop分支存放开发的阶段性成果,从fetaure分支合并,是可以提供测试部署的代码,做人工测试部署,或执行自动化测试的代码源。
feature分支,是做具体功能块开发的分支,项目开发常用敏捷开发,敏捷开发中的用户故事,拆分成多个1-2天的功能模块,分配到开发者,开发者可在本地基于develop建立对用模块分支feature/xxxx,本地开发完成后,并入develop,并入后该特性分支可删除 。
hotfix分支,是针对生产版遇到异常或严重问题必须及时修复的场景,此时要基于master分支,建立hotfix/xxxx 分支,用作修正生产bug,而不影响develop分支其他迭代开发工作,修复后的hotfix分支,验证无误则及时合并至master并tag,及时发版,同时合并至develop,保证后续迭代的正常开发,并入后该hotfix 分支可删除。
模块化
模块化是将大体量功能块拆分至小的可维护单元块,以便分治和复用,是大型复杂应用快速开发和维护的必要基础。
css预处理的变量机制、嵌套关系等语法促进了css样式的书写清晰及便利,为样式模块化提供了基础,加之像webpack配置css-loader来统一加hash 属性,利用属性选择器的特性,避免样式的全局污染,实现其模块化,而对共有样式、属性值,则提取至global模块,其他相应模块引入使用,global模块在主题切换上有很高效的体现。
// webpack.config.js{ test: /\.css$/, use: [ { loader: 'css-loader', options: { modules: true, localIdentName: '[path][name]__[local]--[hash:base64:5]' } } ]}
市面上优秀的css预处理有less、sass、stylus等,各有优缺,选择一个适合团队的即可。
js 模块化,目前常用的是commonjs 规范和es6 的modules, nodejs在commonjs范式基础上制定的模块API,用于node 包的管理,例 modules.export = somePackage / require('somePackage');es6是ECMA2015版本规定的module机制, 通过 export / import 导出引入模块。
node模块
// log.jsmodule.exports = function (mes) { console.log(mes)}// print.jsconst log = require('./log.js')log('hello world')
node 模块按出处分为两大类,一类是内置模块,一类是自定义模块。
内置模块是node API中涉及的模块,由官方提供,如文件系统fs、协议http/https、路径path等。
自定义模块,非官方出品,来自社区或私有自研自用,通常开源的模块都会发布到npm上,开发者可以通过npm install,下载安装(npm install xxxx)。
node 模块按功能分为两大类,一类是开发依赖模块,一类是线上生产依赖模块。
开发依赖模块是开发过程中使用到的,增加开发效率和友好性为主(如代码改动实时更新到页面的热加载模块、dev运行时shell输出文字美化的chalk模块等),通过install时加参数 --save-dev, 备录在package.json 的devdependencies下;生产依赖,为线上运行时,所依赖的模块(如日期格式化模块、px2rem转换模块等),同样install时加参数--save,备录在package.json的dependencies下。
通过 require引入模块有一定机制,如果是直接名字,例 require('path'),则首先看是是否在cash中缓存过,即缓存优先级最高(require入参不一样,但索引的资源是同一个,则视为同一个入缓存),没有则判断是不是内置模块,没有的话则去项目node_modules文件夹下查找;如果是路径索引,绝对路径"/"、或相对路径"./""../",例 require('./a');则按路径位置查找对应模块,如果文件没有指定格式,nodejs会试图加载a.js、a.json,a.node,如果以上都不存在,则按照a文件夹依次索引其下的package.json(main字段配置的文件)、index.js、index.json、index.node。
es6模块
// 2. es6 module// log.jsexport default function (mes) { console.log(mes)}// print.jsimport print from './log.js'print('hello world')
export语法规定是对外的出口,必须跟内部变量建立一一对应的关系。如果模块出口只有一个,可以用export default来导出一个没有名字的变量, export 也可以暴露多个出口,但不可以出现在块级作用域内,否则会出错。
// export.js// 报错export 9// 报错var num = 9export num// 正例一export var num = 9//正例二var age = 15export { age }//正例三 通过as关键词 重命名对外出口var color = 'red'export {color as pigment }// 正例四 default export default function sayHello () { console.log('hello')}
import引入包,语法合export,可以全包引入,可以部分引入,对default 形式的可自定义名引入,import后的from指定文件位置,用法类似require,示例代码如下:
// import.js// 引入的对象可以结构, 对应变量为import 里定义的变量名// from的文件格式后缀可省了import { num } from './export.js' // 重命名引入import { age as year } from './export.js'// 整体引入import * as obj from './export.js' // default 引入import anyName from './export.js'
js 模块化,不管是公共方法还是业务逻辑都分治处理,为便利开发和后期维护,同时也很好的解决了全局变量污染、混乱开发的局面,对大型工程项目开发来讲,尤为重要。组件化
对一个应用来讲,单样式和js模块化,还是不够,需要按照UI用户交互层面进行细颗粒度的组件划分抽离,讲一个页面中不同块分治为不同的组件,其应包含该组件的样式和逻辑数据层块。组件相对独立,组件与组件可自由组合,页面是组件的容器,在不同的业务需求下,快速响应组合。理论上分块后的组件间无时间顺序的依赖,可保证开发的同步进行,在分工中无障碍伸缩,提高生产力。
组件统一放在components下,如下图,每一个组件的样式、标签内容、逻辑归类一起,高内聚,功能明确,目录清晰,使得构建可靠的组件,自然也就可复用。

header.html
header
header.css
.header{ background: black, color: red}
header.js
const title = document.querySelector('.title')title.innerHTML = 'newTitle'
开发中一般不会直接上述原生方式写组件,以目前框架为主的开发方式下,各家框架都有很好配合工程化组件的实现。比如vue 的组件,一个文件构建组件,template标签内dom节点,script里放置js 数据和逻辑,style里样式,通过props定义组件被父级调用时的属性,通过emit实现组件向父级传递数据。
title组件vue示例:
{{title}}
{{color}}
export default {
data() {
title: 'title'
},
props: {
color: {
default: 'red',
type: String
}
}
}
.header{
background: #f0f
}
市面上,各大厂商基于主流框架开源了不少的优秀UI组件库,如基于vue的element-ui、muse-ui等, 基于react 的ant-design 、material-ui等,详细文档可去相应官网查看。
编译构建
前端工程化,围绕在如何提高开发效率,而最终的发布,则依赖于宿主环境浏览器,当下的主流浏览器,可支持的html5、css3、es5,少数es6语法支持,那上述所提及的 css预处理器、原生es6+、js框架、模块机制等需要进行一步编译构建工作,将开发者友好的语言转译成浏览器可识别的资源,目前市面上用的最多的是 webpack。

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundlehttps://www.webpackjs.com/concepts/
从webpackv4.0.0 的开始,可以无配置化编译打包,默认的入口文件是src/index.js,打包后的文件为dist/main.js。当然webpack依然可高度配置。
webpack有四个核心配置项,entry、output、loader、plugin,webpack就如上图中的box, 通过入口entry 文件,输入资源,box 通过loader 和 plugin,然后output出资源。入口出口比较容易理解,下面说下loader和plugin。
loader是用来处理各种类型资源转化成webpack可理解的模块,loader配置中有两个要点,test 和 use。test 正则匹配文件类型,use 配置所要用到的对应loader。当链式调用多个 loader 的时候,请记住它们会以相反的顺序执行。取决于数组写法格式,从右向左或者从下向上执行。
// webpack.config.jsconst path = require('path');const config = { entry: 'index.js', output: { filename: 'bundle.js' }, module: { rules: [ { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] } ] }};module.exports = config;
这里就是告诉webpack在遇到require()/import 对应.less文件类型的资源时,先使用less-loader将less文件转换成css文件,css-loader去解析css文件,遇到"@import"、"url()"这类语句就将相应的样式资源引入,最后由style-loader生成一个包含最终解析完的css代码的style标签,并追加到head标签里。
loader处理特定文件的转换,而plugin则可以处理更广泛的任务,如打包优化、代码压缩、自动插入打包好的js到模板html、环境变量定义等。插件的用法,通过require过来,实例化放入plugins数组中。
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装const config = { plugins: [ new HtmlWebpackPlugin({template: './src/index.html'}) ]};module.exports = config;
webpack 配置中使用插件是简单直接的,多数可以通过文档介绍直接配置使用。自动化再谈提高效率,编译之后我们有了浏览器可识别的代码,那后续工作是否可以缩短到用户的距离,答案是肯定的,如切换测试生产环境、测试、部署等环节,以自动化为方向,在越来越多的项目中,解放人力。
切换环境通过npm run build 指令,先set env后build,代码中通过env走分流逻辑处理不同环境下的代码走向。
// 命令行cross-env NODE_ENV=production webpack --config build/webpack.config.js// 逻辑代码块const env = process.env.envif (env === 'production') { // doSomething in production} else { // doSomething in test}
环境变量设置,因为操作系统不通过而设置上有差异,此处推荐使用cross-env, 免除不同操作系统下来回切换命令的烦恼。
测试
常用的单元测试和验收测试。单元测试是直接测试代码单元,可以一个模块或函数,常用工具mocha。验收测试是用脚本控制浏览器来触发web程序的功能,来测试界面和功能是否完好。目前公司内部测试团队已经开始着手这方面的工作。
部署 生产服务器,如果多台,手工上传代码,将是一件低效且容易出错的;重复性的事情交给机器,自动化部署,可使用Jenkins,它 是开源CI&CD软件领导者, 提供超过1000个插件来支持构建、部署、自动化, 满足任何项目的需要。总结:稳定高效的交付产品,是前端工程化的意义所在,前端发展这么多年,从最初的原始蛮荒,一步一步走到现在高度工具化产线链,不断创新,来满足日益增长的移动化应用场景和多维度客户更高的体验诉求。
A B O U T I N S T R U C T O R
讲 师 速 递
袁森,泰康集团数据信息中心移动互联部前端高级工程师,在前端领域浸润多年。基础扎实,无论在H5,CSS,JS三大框架,或微信小程序weex,还是node等领域均有深入研究。更关键的是小伙人很帅



科技鱼系列
第一期 编码有故事
第二期 计算精度与Java编译问题探究
第三期 流量防卫兵Sentinel
第四期 科普区块链