本文共 15265 字,大约阅读时间需要 50 分钟。
最近这些年前端相关的技术的发展速度犹如坐上了火箭一般一日千里,新技术新框架层出不穷:Node.js、React、Angular、Vue... 端上有React Native、node-webkit,JavaScript竟然隐约有一统前端、后端、移动端、PC端之势...
不过与其说进击的JavaScript,我更觉得不如说天下技术本一家。就像“前端开发”“后端开发”“客户端开发”等各种开发之间并没有那么大的区别、只是对技术的细分,不同的编程语言、工具也都是在朝着高性能、可复用、开发友好的方向前进。JavaScript也是在朝着这个方向前进,良好的生态和无数开发人员的努力使得它迎来了百花齐放的今天。这几年作为“Java后端开发”的我其实做Windows C++客户端开发的时间和写Java后端的时间几乎是五五开(对,开挂般的工种...),前端技术其实以前也是我的心头爱,不过这几年算是荒废了,只能看着前端同学的IDE流口水再厚着脸皮问“这几行代码是什么意思啊?”,前端GG心情好还会耐心解释一番、要是心情不好就只能接收到“说了你也不懂”的眼神了...
最近终于有些时间,可以系统地学习梳理一遍前端知识了,这篇文章既是学习成果的自我总结,也希望能对有兴趣的同学有所裨益,由于我本身也还只是个前端菜鸟,文章如果有错误或不当之处还望大家斧正。如前面所说,这篇文章分享给有兴趣的、对前端技术不太熟悉的同学。但你应该至少掌握了以下基础知识(否则需要先充下电了哦):
如果你能熟练使用jQuery、Bootstrap,那么恭喜你,作为一个上古时代的前端高手,我相信你可以无障碍地阅读下面的内容!
下面是相关的参考资料,本文充其量只是一个速成教程,真正的精华都在下面:
下文中还直接使用了很多阮一峰老师著作中的内容,有些不成段落的语句可能没有标记成引用还望谅解。
JavaScript的核心语法部分相当精简,只包括两个部分:基本的语法构造(比如操作符、控制结构、语句)和标准库(就是一系列具有各种功能的对象比如Array、Date、Math等)。
不同的运行环境(如浏览器、Node.js)也会提供额外的API供JavaScript调用,以浏览器为例,它提供的额外API可以分成三大类:这一部分只介绍JavaScript的核心语法,示例代码基本上都可以在Chrome或其它浏览器的开发者工具的Console中执行。
1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给国际标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。
阮一峰:《ECMAScript 6 入门》
简而言之,ECMAScript是标准,JavaScript是其实现(另外的ECMAScript方言还有Jscript和ActionScript)。
这好比Java标准也由JCP(Java Community Process)在维护,因此既有Oracle JDK,还有OpenJDK、IBM JDK。由于时间跨度、浏览器兼容性等原因,这篇文章将以2015年正式发布ECMAScript 6标准(也即ECMAScript 2015)作为基础。与“上古时代”的JavaScript(ECMAScript 5于2009年发布)相比,ECMAScript 6加入了众多的新特性,我认为这也是这些年JavaScript腾飞的基础之一。
自ECMAScript 6开始ECMAScript将每年发布一个版本,ECMAScript 2016、2017也已在当年发布,不过其跨度不大、改变也不算多,有兴趣的同学可以自行学习。这么看来ECMAScript 6的意义相当于C++11,同样是时隔多年(C++11之前的一个标准是C++03...),同样是堪称大刀阔斧地“重新定义”...
ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。
阮一峰:《ECMAScript 6 入门》
如下所示,"let"只在其所在的大括号范围内有效,可以视为“局部变量”。
{ let a = 10; var b = 1;}a // ReferenceError: a is not defined.b // 1
另外不同于在Scala、Swift中"let"用来表示不变量(不要跟ES6记混了哦),ES6还是很常规地引入了"const"来表示常量:
const PI = 3.1415;PI = 3;
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
阮一峰:《ECMAScript 6 入门》
先看数组的解构,还是很简单的:
let [a, b, c] = [1, 2, 3];a //1b //2c //3
再看对象解构并赋值到变量,初看也比较简单:
let { foo, bar } = { foo: "aaa", bar: "bbb" };foo // "aaa"bar // "bbb"
但如果变量名与属性名不一致,必须写成下面这样:
let { foo: baz } = { foo: "aaa", bar: "bbb" };baz // "aaa"foo // error: foo is not defined
上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。你记住了吗?
在ES6之前,生成实例对象的方法是通过构造函数。下面就是这样一个反人类的例子。
function Point(x, y) { this.x = x; this.y = y;}Point.prototype.toString = function () { return '(' + this.x + ', ' + this.y + ')';};var p = new Point(1, 2);
是的,ES6英雄般地又一次拯救了我们的脑细胞,请看下面的例子!没错,那个constructor函数代表了类的构造方法(也可以不显式定义哦)。
//定义类class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; }}var point = new Point(2, 3);point.toString();
不多说,直接上代码,先看Proxy的例子,Proxy支持的拦截操作共13种,这里只展示它拦截get的操作。
let person = { name: "张三"};let proxy = new Proxy(person, { get: function(target, property) { if (property === 'name') { return "李四"; } else { return "你猜"; } }});person.name // "张三"proxy.name // "李四"proxy.age // "你猜"
再看Reflect的,与Proxy对应、它也有13种方法,这里也只展示对get的反射。
var myObject = { foo: 1, bar: 2, get baz() { return this.foo + this.bar; },}Reflect.get(myObject, 'foo') // 1Reflect.get(myObject, 'bar') // 2Reflect.get(myObject, 'baz') // 3
作为这几年的大热门,lambda不出意外地也被加入到了ES6中,不过它的正式名称是Arrow Functions即箭头函数。其示例如下。
//箭头函数var sum = (num1, num2) => num1 + num2;// 等同于var sum = function(num1, num2) { return num1 + num2;};
有没有发现这个箭头(=>)是用等号写的箭头,不同于Java里面那个横线箭头(->)?让我们来复习下不同语言中的lambda表达式吧:
[](int a) -> bool { return a > 0; } // C++,此例可以省略箭头和"bool"返回值类型声明a -> a > 0 // Javaa => a > 0 // C#a => a > 0; //JavaScriptlambda a: a > 0 # Python(lambda (a) (> a 0)) ;; Lisp
Promise是ES6中提出的异步编程的一个解决方案,Java中与之接近的概念是Future(你也可以用Future来实现Promise机制,在Scala、Netty中也实现了Promise机制)。但也只能是接近,因为JavaScript语言本身是单线程的(不讨论Web Worker)、永不阻塞的、基于事件循环模型(Event Loop)的,用单纯Java的思想会很难理解下面这段代码的执行结果为什么是“2 1”:
setTimeout(function(){console.log(1);}, 0);console.log(2);
言归正传,ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。下面代码创造了一个Promise实例。
const promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); }});
你可能会问“resolve”、“reject”是什么?可以说它们代表了“当异步操作成功时的回调函数”、“当异步操作失败时的回调函数”,它们现在还未定义,它们会在后面由你在“.then”方法中传入。
先看一个简单的例子,这里在then方法中只传入了resolve函数,未传入reject回调函数。
let promise = new Promise(function(resolve, reject) { console.log('Promise'); resolve();});promise.then(function() { console.log('resolved.');});console.log('Hi!');// Promise// Hi!// resolved
再看一个Ajax的例子,好好理解下哦。
const getJSON = function(url) { const promise = new Promise(function(resolve, reject){ const handler = function() { if (this.readyState !== 4) { return; } if (this.status === 200) { resolve(this.response); } else { reject(new Error(this.statusText)); } }; const client = new XMLHttpRequest(); client.open("GET", url); client.onreadystatechange = handler; client.responseType = "json"; client.setRequestHeader("Accept", "application/json"); client.send(); }); return promise;};getJSON("/posts.json").then(function(json) { console.log('Contents: ' + json);}, function(error) { console.error('出错了', error);});
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。
阮一峰:《ECMAScript 6 入门》
简单地说,Java有import、Python有import、C++有#include,可JavaScript一直没法“引用”别的js文件。在此背景下,出现了:
怎么样,头晕了没?还好自从有了ES6,生命里都是奇迹... ES6在语言层面实现了模块功能,完全可以一统服务端和客户端取代CommonJs/AMD/CMD,我们可以告别那个八仙过海各显神通的时代了,感谢ES6为我们的防脱发事业做出的卓越贡献!
上面纯属玩笑,无论如何,我们都要感谢为了JavaScript模块化标准做出过贡献的各位大神,他们的探索无疑为ECMAScript的模块化标准做出了不可磨灭的贡献!
言归正传,现在最新的主流浏览器都已经支持ES Module,Node.js也开始支持ES Module标准,我们现在可以只学习ES6的Module了,不用再关注CommonJs/AMD/CMD等之前的规范。
让我们先在profile.js中用export来导出一组变量,注意除此之外还可以导出函数和类:// profile.jsvar firstName = 'Michael';var lastName = 'Jackson';var year = 1958;export {firstName, lastName, year};
再在main.js中用import来导入它们:
// main.jsimport {firstName, lastName, year} from './profile';function setName(element) { element.textContent = firstName + ' ' + lastName;}
是不是很简单呢?
这两个数组的方法是早就在2011年发布的ECMAScript 5.1中就发布的,但我觉得还是有必要提一下的。它们的作用也相当于Java8的Stream API中的map和reduce(及collect),示例如下:
// map示例let numbers = [1, 5, 10, 15];let doubles = numbers.map( x => x ** 2);// doubles is now [1, 25, 100, 225]// numbers is still [1, 5, 10, 15]//reduce示例,后面那个"0"表示给sum的初始值var total = [0, 1, 2, 3].reduce(function(sum, currentValue) { return sum + currentValue;}, 0);// total is 6
你应该有注意到在上面的map示例中使用了箭头函数,如果你找茬能力强力的话,应该发现了当数组的map、reduce方法发布时,那时还没有箭头函数呢!
那么问题来了,在没有箭头函数的时候,map、reduce是怎么用的呢? 答案很简单,当年用的是普通的回调函数:var numbers = [1, 5, 10, 15];var doubles = numbers.map(function(value){return value ** 2;});// doubles is now [1, 25, 100, 225]// numbers is still [1, 5, 10, 15]
Node.js是一个JavaScript运行环境(runtime),发布于2009年5月,由Ryan Dahl开发,实质是对Chrome V8引擎进行了封装。
简单地说,Node.js是在浏览器之外的一个JavaScript运行时,在安装了Node.js之后,你就可以在命令行中执行JS了:
echo "console.log('Hello Node.js!');" > index.jsnode index.js//Hello Node.js!
与浏览器一样,Node.js也为JavaScript提供了大量的额外API,如 fs(文件系统)、net(网络)、os(操作系统)等等。可以说在Node.js的加持下,JavaScript拥有了类似于Python的能力。
如何安装Node.js就不详述了,Mac下可以使用brew安装,Windows下也有安装包。
注意我安装的node已经是v9.3.0了,此章的Demo是以该版本为基本的。如果还是要类比的话,npm类似于Java体系中的Maven,负责进行包管理。
有个不同之处在于Node.js已经自带了npm,不需要再额外安装了。在使用之前,可以先做件事情,将npm的切换到淘宝镜像,可以有效地解决下载包太慢的问题:
npm config set registry " https://registry.npm.taobao.org"
在使用"npm install xxx"命令时(这里xxx仅为示例),可以有一个"-g"的参数,它表示将xxx安装到全局目录(我这是/usr/local/lib/node_modules)下,同时用"which xxx"命令可以发现在"/usr/local/bin"下面已经有了一个xxx的软链接,这样我们就可以在任何目录下执行tnpm命令了。
如果不加"-g"呢? 那包就会被安装到当前目录的"node_modules"目录下,记住这一点,下面我们的Demo马上就要用到。
如果说npm类似于Maven,那么package.json就是pom.xml了,里面同样记录了项目信息及依赖。
有了上面这些知识之后,我们马上就可以开始实现一个Web服务器,目前使用Node.js下比较广泛的Web框架是Express,好比Python里面的Django。
你一定不想把你的home目录弄乱是吧?
mkdir ExpressStartercd ExpressStarter
用npm init可以直接创建模块,生成package.json,npm会询问你包名、版本、入口点、Git仓库之类的,直接回车可以使用默认值,但在此Demo中入口点(entry point)请使用"index.mjs",后面我会说原因的。
npm initpackage name: (expressstarter) com.spirit.testversion: (1.0.0)description:entry point: (index.js) index.mjstest command:git repository:keywords:author:license: (ISC)
此时的package.json内容如下:
{ "name": "com.spirit.test", "version": "1.0.0", "description": "", "main": "index.mjs", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC"}
用一行命令就可以搞定了:
npm install --save express
没错,这行命令的意思就是在当前目录的"node_modules"下安装express(及其所有依赖),并将express依赖写到package.json中去,是不是很方便?
此时的package.json的内容如下,express前面那个"^"相信你能猜到是什么意思:
{ "name": "com.spirit.test", "version": "1.0.0", "description": "", "main": "index.mjs", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "express": "^4.16.2" }}
接下来就可以开始搬砖了,请在ExpressStarter文件夹下创建index.mjs文件,再掏出你最爱的IDE/编辑器,把下面的代码放进去。Demo代码仅仅11行,相信不用解释你也能看懂。
import express from 'express'let app = express();app.get('/', function (req, res) { res.send('Hello World!');});app.listen(3000, function(){ console.log('Example app listening on port 3000!');});
使用下述命令即可运行我们的Demo:
node --experimental-modules index.mjs
如果你看到了"Example app listening on port 3000!"这行提示,那么马上打开浏览器访问"" 看看效果吧!
你应该注意到了"--experimental-modules"这个参数,它跟我们使用".mjs"的文件名后缀的原因一样:因为Node.js之前使用CommonJs规范,在引用其它模块是使用的是"require"关键字,这里我们直接使用了ES6的"import",所以需要开启这项目前还是实验性的功能,而为了与以前保持兼容、使用ES6 Module规范的模块都需要使用".mjs"作为后缀名。
Demo到这里就演示结束了,你心里可能有些疑惑,但如果想要深入钻研就要靠你自己了。
但可以多说一些的是,在常规项目中,"node_modules"文件夹是要被加入到.gitignore里面去的、不要把它一起提交到git上去,其他同学在clone该项目后,直接在项目文件夹下执行"npm install"就可以把依赖下载到"node_modules"文件夹里面了。
而且在package.json的"scripts"中可以加入编译、清理之类的命令,其作用大致相当于mvn compile、mvn clean。先告诉大家一个不幸的消息:
上面的内容其实还只介绍了JavaScript语法、Node.js及项目基本结构,还有一个用Node.js开发Web服务的Demo,可以说跟“前端开发”还没有半毛钱的关系。但是!Node.js、npm、package.json是现代前端工程的基石,我们需要它们来完成对前端工程的构建(包括“编译”、打包、测试等等)。
下面我们先来学习大名鼎鼎的React,并以此为出发点来学习各种前端构建工具。
React是起源于Facebook的一个前端框架,虚拟DOM技术是其基石。React提出了JSX语法,而组件化是其核心思想。
先抛开Node.js那些东西,让我们以传统的方式在在页面中引入React。你可以将下面的html代码拷贝下来放到一个htm文件里面去,再用浏览器打开看看效果。
React
来看html中的那段JavaScript, "<h1>Hello, world!</h1>"是什么? 它不是字符串,因为没有引号,很明显它也不像是一个“正常的对象”。 这就是React独有的JSX语法,是React创建虚拟DOM的一种方式,这个h1标签在稍后会被“编译”(或称翻译)为一个真正的JavaScript对象。
可以发生html中的那个script标签的type是"text/babel"、而常规的JavaScript的type是"text/javascript",这是为什么? 这是因为JSX语法很明显和JavaScript是不兼容的,所以用到了JSX的地方都声明为"text/babel"。
那babel又是什么?Babel是一个JavaScript“编译器”,它可以使用了ES6特性的JavaScript代码编译为符合ES 5.1标准的代码,也可以将JSX语法编译为符合JavaScript语法的代码,这里我们就是使用它来编译了含JSX的代码。
需要注意的是,在实际情况下编译这一步是预先完成的、而不是像本Demo这样在客户端加载babel来完成,在Chrome的控制台中你也可以看到Babel输出的警告,不过这一章我们只讲“传统”方式,先不讲构建。
React中的组件是其基本设计思想,通过组件与JSX的组合,更是可以发挥出无穷的威力。那接下来就把Demo改造成一个使用组件完成试试,这里只展示JavaScript代码了:
HelloMessage类中的render()方法就好比各种GUI库里面的paint方法,在界面需要绘制时会被调用。
那props怎么来的? 在此例中,React会将{name:"Spirit"}作为props传给HelloMessage组件。React 把组件看成是一个状态机(State Machines)。通过与用户的交互,实现不同状态,然后渲染 UI,让用户界面和数据保持一致。
简单地说,每个组件除开有props属性外,还有一个state属性,当state发生改变时,会触发组件的render()方法。所以我们再来继续修改Demo,实现在点击时切换内容显示。在此例中,我们实现了构建方法并显式地接收了props,还调用了super方法(与Java一样,这应该是构建方法中的第一行代码),并设置了state对象的值,当组件被点击时、state会被改变、render方法也会被再次调用。
其中的"onClick"是JSX的语法,与传统的HTML写法有所不同,具体的知识需要详细学习JSX语法及事件相关的知识。在上一章我们学习了React的基础知识,但我们都知道现在前端js与我们的html页面是分离的,而且前端发布时一般都需要先进行构建,那么前端构建又是怎么回事呢? 让我们继续用上面的Demo来管窥一番吧。
Gulp是现在流行的前端构建工具,其作用类似于Java体系中的Maven...等等,之前不是说npm也相当于Maven吗,为什么要使用Gulp而不直接使用npm scripts? 按我的理解,npm scripts更通用更灵活更底层,而Gulp更适合应对前端资源构建。
接下来我们就一步一步地搭建构建环境,来构建上一章的那个React Demo。
很常规地,创建文件夹,npm init即可。
mkdir ReactStartercd ReactStarternpm init
通过npm安装react、react-dom的Node.js包
npm install --save react react-dom
在项目目录下创建"index.htm"文件,其内容如下:
React
在项目目录下创建"src"目录,在其下新建"index.js"文件,内容如下:
import React from 'react';import ReactDOM from 'react-dom';class HelloMessage extends React.Component { constructor(props) { super(props); /* * 如果不写这行在下面的handleClick方法中获取到的this指针将是undifined。 * 具体原因可以看阮一峰老师的《ECMAScript 6 入门》。 */ this.handleClick = this.handleClick.bind(this); this.state = {liked: false}; } handleClick() { this.setState({liked: !this.state.liked}); } render() { let text = this.state.liked ? 'like' : "don't like" returnHello, I {text} {this.props.name}
; }}ReactDOM.render(, document.getElementById('root'));
前面有提到,JSX语法不是JavaScript标准、ES6也不一定是所有浏览器都兼容的,我们需要使用Babel来进行“编译”。
首先是通过npm安装Babel及React、ES2015(即ES6)插件,注意指令是"--save-dev",将它们安装为开发依赖。
npm install --save-dev babel-cli babel-preset-react babel-preset-es2015
然后在项目目录下新建".babelrc"文件,这是Babel的配置文件,其内容如下:
{ "presets": [ "react", "es2015" ], "plugins": []}
同样地,先安装Gulp及其Babel插件到开发依赖中去:
npm install --save-dev gulp gulp-babel
然后在项目目录下新建"gulpfile.js"文件,这是Gulp的配置文件,其内容如下所示,注意下这里仍然使用Node.js的"require"而不是"import"。
const gulp = require('gulp');const babel = require('gulp-babel');gulp.task('default', function() { // 将你的默认的任务代码放在这 gulp.src(['src/*.js']) .pipe(babel()) .pipe(gulp.dest('build'));});
好了,Gulp和Babel都配置好了,在项目目录下执行下"gulp"命令构建一下试试。
在build目录下我们找到了"index.js",用编辑器打开其内容,可以发现原来的JSX语法和ES6语法确实被“编译”了,但却是被编译为了Node.js的语法,如"require",React也还是作为一个Node.js依赖没有被编译到一起,这样的编译结果无法在浏览器中正常使用,那要怎么办呢?Webpack是一个前端资源打包工具,它能够对模块的依赖关系进行分析,并将这些资源都打包成可在浏览器上运行的形式。
同样,先安装Webpack的依赖:
npm install --save-dev gulp-webpack
再来修改下gulpfile.js,加上Webpack的流程:
const gulp = require('gulp');const babel = require('gulp-babel');const webpack = require('gulp-webpack');gulp.task('default', function() { // 将你的默认的任务代码放在这 gulp.src(['src/*.js']) .pipe(babel()) .pipe(gulp.dest('build')) .pipe(webpack({ output:{filename:'bundle.js'} })) .pipe(gulp.dest('build'));});
再执行"gulp"命令,"build"目录下会生成"bundle.js",其中的内容整合了"index.js"及其所有依赖。
现在打开项目目录下的"index.htm",页面应该是运行正常的,这下我们实现了一个初具雏形的前端构建环境。此时可以再修改一个"package.json",去掉里面的"main"这项,再往"script"中加入"build"脚本:
"scripts": { "build": "gulp"}
这样当执行"npm run build"命令时,实际上也是启动了gulp进行构建工作。
需要注意的是,这个构建的Demo配置得比较简单,代码没有压缩、混淆,也没有去除注释,如果你在代码里面写了什么不够友善的注释、在单量之类的数据上用了随机数,小心被用户一眼就看出来了哦。
实际的前端工程配置会比这复杂不少,各个BU、各个前端团队目前应该也有不同的构建工具及配置,革命尚未成功,同志仍需继续学习!
这篇文章只是我作为一个前端菜鸟、浅尝辄止地学习了一下前端相关的部分知识后的一个总结,相信既不全面、也会有不少错误之处,发出来一是希望能给同样对前端技术感兴趣的同学一点参考,也是希望能接受到大家的指导。
前端技术仍在飞速发展,想要真正的做到有些理解和感悟,还得不断地学习与练习,学而时习之,不亦说乎?转载地址:http://ftezx.baihongyu.com/