写在前面
前言
最近这些年前端相关的技术的发展速度犹如坐上了火箭一般一日千里,新技术新框架层出不穷:Node.js、React、Angular、Vue... 端上有React Native、node-webkit,JavaScript竟然隐隐约有一统前端、后端、移动端、PC端之势... 不过与其说进击的JavaScript,我更觉得不如说天下技术本一家。就像“前端开发”“后端开发”“客户端开发”等各种开发之间并没有那么大的区别、只是对技术的细分,不同的编程语言、工具也都是在朝着高性能、可复用、开发友好的方向前进。JavaScript也是在朝着这个方向前进,良好的生态和无数开发人员的努力使得它迎来了百花齐放的今天。
写这篇文章的原因
这几年作为“Java后端开发”的我其实做Windows C++客户端开发的时间和写Java后端的时间几乎是五五开(对,开挂般的工种...),前端技术其实以前也是我的心头爱,不过这几年算是荒废了,只能看着前端同学的IDE流口水再厚着脸皮问“这几行代码是什么意思啊?”,前端GG心情好还会耐心解释一番、要是心情不好就只能接收到“说了你也不懂”的眼神了... 最近终于有些时间,可以系统地学习梳理一遍前端知识了,这篇文章既是学习成果的自我总结,也希望能对有兴趣的同学有所裨益,由于我本身也还只是个前端菜鸟,文章如果有错误或不当之处还望大家斧正。
面向的对象
如前面所说,这篇文章分享给有兴趣的、对前端技术不太熟悉的同学。但你应该至少掌握了以下基础知识(否则需要先充下电了哦):
- HTML,至少得能手写个html、header、body吧。
- CSS,我知道大家都烦这个、下面我也不会说它的,但你至少应该知道怎么在html中插入/引用css吧,再给body设置个背景色试试?
- JavaScript,我指的是最基础的,比如知道怎样在html中插入/引用JavaScript,能看懂document.getElementById('demo').innerHTML='Hello JavaScript'; 这行代码。
- 表单提交与Ajax,是不是就是一个刷新一个不刷新?... 但你至少应该能用JavaScript写出Ajax提交数据的Demo来。
如果你能熟练使用jQuery、Bootstrap,那么恭喜你,作为一个上古时代的前端高手,我相信你可以无障碍地阅读下面的内容!
参考资料
下面是相关的参考资料,本文充其量只是一个速成教程,真正的精华都在下面:
- JavaScript 标准参考教程 ,阮一峰老师著。
- ECMAScript 6 入门,还是阮一峰老师著,此书有纸质版可以支持下哦。
- MDN JavaScript 参考文档,Mozilla的JavaScript参考文档,信Firefox、得永生。
下文中还直接使用了很多阮一峰老师著作中的内容,有些不成段落的语句可能没有标记成引用还望谅解。
基础:JavaScript有些不一样了
JavaScript的核心语法部分相当精简,只包括两个部分:基本的语法构造(比如操作符、控制结构、语句)和标准库(就是一系列具有各种功能的对象比如Array、Date、Math等)。 不同的运行环境(如浏览器、Node.js)也会提供额外的API供JavaScript调用,以浏览器为例,它提供的额外API可以分成三大类:
- 浏览器控制类:操作浏览器,如window.open()。
- DOM 类:操作网页的各种元素,如getElementById()。
- Web 类:实现互联网的各种功能,如XMLHttpRequest。
这一部分只介绍JavaScript的核心语法,示例代码基本上都可以在Chrome或其它浏览器的开发者工具的Console中执行。
ECMAScript与JavaScript
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。
ECMAScript 6(下述简称ES6)
由于时间跨度、浏览器兼容性等原因,这篇文章将以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...),同样是堪称大刀阔斧地“重新定义”...
哪里不一样
let:你还在用"var"吗?
ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。 阮一峰:《ECMAScript 6 入门》
如下所示,"let"只在其所在的大括号范围内有效,可以视为“局部变量”。 1
2
3
4
5
6
7{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 11
2
3const PI = 3.1415;
PI = 3;
解构赋值:我连赋值都快看不懂了
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。 阮一峰:《ECMAScript 6 入门》
先看数组的解构,还是很简单的: 1
2
3
4let [a, b, c] = [1, 2, 3];
a //1
b //2
c //31
2
3let { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"1
2
3let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined
Class:终于有正宫对象了
在ES6之前,生成实例对象的方法是通过构造函数。下面就是这样一个反人类的例子。 1
2
3
4
5
6
7
8
9
10function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//定义类
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、Reflect:动态代理和反射不只Java有哦
不多说,直接上代码,先看Proxy的例子,Proxy支持的拦截操作共13种,这里只展示它拦截get的操作。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17let person = {
name: "张三"
};
let proxy = new Proxy(person, {
get: function(target, property) {
if (property === 'name') {
return "李四";
} else {
return "你猜";
}
}
});
person.name // "张三"
proxy.name // "李四"
proxy.age // "你猜"1
2
3
4
5
6
7
8
9
10
11var myObject = {
foo: 1,
bar: 2,
get baz() {
return this.foo + this.bar;
},
}
Reflect.get(myObject, 'foo') // 1
Reflect.get(myObject, 'bar') // 2
Reflect.get(myObject, 'baz') // 3
lambda:请叫我“箭头函数”
作为这几年的大热门,lambda不出意外地也被加入到了ES6中,不过它的正式名称是Arrow Functions即箭头函数。其示例如下。 1
2
3
4
5
6
7//箭头函数
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};1
2
3
4
5
6[](int a) -> bool { return a > 0; } // C++,此例可以省略箭头和"bool"返回值类型声明
a -> a > 0 // Java
a => a > 0 // C#
a => a > 0; //JavaScript
lambda a: a > 0 # Python
(lambda (a) (> a 0)) ;; Lisp
Promise:异步编程你怕不怕
Promise是ES6中提出的异步编程的一个解决方案,Java中与之接近的概念是Future(你也可以用Future来实现Promise机制,在Scala、Netty中也实现了Promise机制)。但也只能是接近,因为JavaScript语言本身是单线程的(不讨论Web Worker)、永不阻塞的、基于事件循环模型(Event Loop)的,用单纯Java的思想会很难理解下面这段代码的执行结果为什么是“2 1”: 1
2setTimeout(function(){console.log(1);}, 0);
console.log(2);1
2
3
4
5
6
7
8
9const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});1
2
3
4
5
6
7
8
9
10
11
12
13
14let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29const 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);
});
Module:官方模块化,远离CommonJs/AMD/CMD吧!
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。 阮一峰:《ECMAScript 6 入门》
简单地说,Java有import、Python有import、C++有#include,可JavaScript一直没法“引用”别的js文件。在此背景下,出现了:
- CommonJs规范,用于服务端,Node.js的require是其实现。
- AMD(Asynchronous Module Definition)规范,用于浏览器环境,RequireJS是其实现。
- CMD(Common Module Definition)规范,由玉伯老师提出,用于浏览器环境,Sea.js是其实现。
怎么样,头晕了没?还好自从有了ES6,生命里都是奇迹... ES6在语言层面实现了模块功能,完全可以一统服务端和客户端取代CommonJs/AMD/CMD,我们可以告别那个八仙过海各显神通的时代了,感谢ES6为我们的防脱发事业做出的卓越贡献! 上面纯属玩笑,无论如何,我们都要感谢为了JavaScript模块化标准做出过贡献的各位大神,他们的探索无疑为ECMAScript的模块化标准做出了不可磨灭的贡献! 言归正传,现在最新的主流浏览器都已经支持ES Module,Node.js也开始支持ES Module标准,我们现在可以只学习ES6的Module了,不用再关注CommonJs/AMD/CMD等之前的规范。 让我们先在profile.js中用export来导出一组变量,注意除此之外还可以导出函数和类: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
再在main.js中用import来导入它们:
// main.js
import {firstName, lastName, year} from './profile';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
其它:数组的map、reduce你造吗?(ECMAScript 5.1)
这两个数组的方法是早就在2011年发布的ECMAScript 5.1中就发布的,但我觉得还是有必要提一下的。它们的作用也相当于Java8的Stream API中的map和reduce(及collect),示例如下: 1
2
3
4
5
6
7
8
9
10
11
12
13// 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 61
2
3
4
5var 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:前端开挂的起点
简介
Node.js是一个JavaScript运行环境(runtime),发布于2009年5月,由Ryan Dahl开发,实质是对Chrome V8引擎进行了封装。 简单地说,Node.js是在浏览器之外的一个JavaScript运行时,在安装了Node.js之后,你就可以在命令行中执行JS了: 1
2
3
4echo "console.log('Hello Node.js!');" > index.js
node index.js
//Hello Node.js!
安装
如何安装Node.js就不详述了,Mac下可以使用brew安装,Windows下也有安装包。 注意我安装的node已经是v9.3.0了,此章的Demo是以该版本为基本的。
npm
如果还是要类比的话,npm类似于Java体系中的Maven,负责进行包管理。 有个不同之处在于Node.js已经自带了npm,不需要再额外安装了。
开始之前
在使用之前,可以先做件事情,将npm的切换到淘宝镜像,可以有效地解决下载包太慢的问题: 1
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马上就要用到。
package.json
如果说npm类似于Maven,那么package.json就是pom.xml了,里面同样记录了项目信息及依赖。
用Node.js写一个Web服务器
有了上面这些知识之后,我们马上就可以开始实现一个Web服务器,目前使用Node.js下比较广泛的Web框架是Express,好比Python里面的Django。
创建一个项目文件夹
你一定不想把你的home目录弄乱是吧? 1
2mkdir ExpressStarter
cd ExpressStarter
初始化项目
用npm init可以直接创建模块,生成package.json,npm会询问你包名、版本、入口点、Git仓库之类的,直接回车可以使用默认值,但在此Demo中入口点(entry point)请使用"index.mjs",后面我会说原因的。 1
2
3
4
5
6
7
8
9
10npm init
package name: (expressstarter) com.spirit.test
version: (1.0.0)
description:
entry point: (index.js) index.mjs
test command:
git repository:
keywords:
author:
license: (ISC)1
2
3
4
5
6
7
8
9
10
11{
"name": "com.spirit.test",
"version": "1.0.0",
"description": "",
"main": "index.mjs",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
安装express并写入package.json依赖
用一行命令就可以搞定了: 1
npm install --save express
1
2
3
4
5
6
7
8
9
10
11
12
13
14{
"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"
}
}
编写index.mjs
接下来就可以开始搬砖了,请在ExpressStarter文件夹下创建index.mjs文件,再掏出你最爱的IDE/编辑器,把下面的代码放进去。Demo代码仅仅11行,相信不用解释你也能看懂。 1
2
3
4
5
6
7
8
9
10
11import 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
使用下述命令即可运行我们的Demo: 1
node --experimental-modules index.mjs
其它说明
Demo到这里就演示结束了,你心里可能有些疑惑,但如果想要深入钻研就要靠你自己了。 但可以多说一些的是,在常规项目中,"node_modules"文件夹是要被加入到.gitignore里面去的、不要把它一起提交到git上去,其他同学在clone该项目后,直接在项目文件夹下执行"npm install"就可以把依赖下载到"node_modules"文件夹里面了。 而且在package.json的"scripts"中可以加入编译、清理之类的命令,其作用大致相当于mvn compile、mvn clean。
React
先告诉大家一个不幸的消息: 上面的内容其实还只介绍了JavaScript语法、Node.js及项目基本结构,还有一个用Node.js开发Web服务的Demo,可以说跟“前端开发”还没有半毛钱的关系。 但是!Node.js、npm、package.json是现代前端工程的基石,我们需要它们来完成对前端工程的构建(包括“编译”、打包、测试等等)。 下面我们先来学习大名鼎鼎的React,并以此为出发点来学习各种前端构建工具。
简介
React是起源于Facebook的一个前端框架,虚拟DOM技术是其基石。React提出了JSX语法,而组件化是其核心思想。
以“传统”的方式将React引入页面
先抛开Node.js那些东西,让我们以传统的方式在在页面中引入React。你可以将下面的html代码拷贝下来放到一个htm文件里面去,再用浏览器打开看看效果。1 |
|
Hello, world!
"是什么? 它不是字符串,因为没有引号,很明显它也不像是一个“正常的对象”。 这就是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输出的警告,不过这一章我们只讲“传统”方式,先不讲构建。
Component-组件
React中的组件是其基本设计思想,通过组件与JSX的组合,更是可以发挥出无穷的威力。那接下来就把Demo改造成一个使用组件完成试试,这里只展示JavaScript代码了: 1
2
3
4
5
6
7
8
9
10
11
12<script type="text/babel">
class HelloMessage extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
ReactDOM.render(
<HelloMessage name="Spirit" />,
document.getElementById('root')
);
</script>
State
React 把组件看成是一个状态机(State Machines)。通过与用户的交互,实现不同状态,然后渲染 UI,让用户界面和数据保持一致。 简单地说,每个组件除开有props属性外,还有一个state属性,当state发生改变时,会触发组件的render()方法。 所以我们再来继续修改Demo,实现在点击时切换内容显示。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29<script type="text/babel">
class HelloMessage extends React.Component {
constructor(props) {
super(props);
/*
* 如果不写这行在下面的handleClick方法中获取到的this指针将是undefined。
* 具体原因可以看阮一峰老师的《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"
return <h1 onClick={this.handleClick}>Hello, I {text} {this.props.name}</h1>;
}
}
ReactDOM.render(
<HelloMessage name="Spirit" />,
document.getElementById('root')
);
</script>
前端构建
在上一章我们学习了React的基础知识,但我们都知道现在前端js与我们的html页面是分离的,而且前端发布时一般都需要先进行构建,那么前端构建又是怎么回事呢? 让我们继续用上面的Demo来管窥一番吧。
Gulp
Gulp是现在流行的前端构建工具,其作用类似于Java体系中的Maven...等等,之前不是说npm也相当于Maven吗,为什么要使用Gulp而不直接使用npm scripts? 按我的理解,npm scripts更通用更灵活更底层,而Gulp更适合应对前端资源构建。 接下来我们就一步一步地搭建构建环境,来构建上一章的那个React Demo。
初始化项目
很常规地,创建文件夹,npm init即可。 1
2
3mkdir ReactStarter
cd ReactStarter
npm init
安装react依赖
通过npm安装react、react-dom的Node.js包 1
npm install --save react react-dom
码代码
在项目目录下创建"index.htm"文件,其内容如下: 1
2
3
4
5
6
7
8
9
10
11
<html>
<head>
<meta charset="utf-8">
<title>React</title>
</head>
<body>
<div id="root"></div>
<script src="build/bundle.js"></script>
</body>
</html>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30import React from 'react';
import ReactDOM from 'react-dom';
class HelloMessage extends React.Component {
constructor(props) {
super(props);
/*
* 如果不写这行在下面的handleClick方法中获取到的this指针将是undefined。
* 具体原因可以看阮一峰老师的《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"
return <h1 onClick={this.handleClick}>Hello, I {text} {this.props.name}</h1>;
}
}
ReactDOM.render(
<HelloMessage name="Spirit" />,
document.getElementById('root')
);
配置Babel
前面有提到,JSX语法不是JavaScript标准、ES6也不一定是所有浏览器都兼容的,我们需要使用Babel来进行“编译”。 首先是通过npm安装Babel及React、ES2015(即ES6)插件,注意指令是"--save-dev",将它们安装为开发依赖。 1
npm install --save-dev babel-cli babel-preset-react babel-preset-es2015
1
2
3
4
5
6
7{
"presets": [
"react",
"es2015"
],
"plugins": []
}
配置Gulp
同样地,先安装Gulp及其Babel插件到开发依赖中去: 1
npm install --save-dev gulp gulp-babel
1
2
3
4
5
6
7
8
9const gulp = require('gulp');
const babel = require('gulp-babel');
gulp.task('default', function() {
// 将你的默认的任务代码放在这
gulp.src(['src/*.js'])
.pipe(babel())
.pipe(gulp.dest('build'));
});
配置Webpack
Webpack是一个前端资源打包工具,它能够对模块的依赖关系进行分析,并将这些资源都打包成可在浏览器上运行的形式。 同样,先安装Webpack的依赖: 1
npm install --save-dev gulp-webpack
1
2
3
4
5
6
7
8
9
10
11
12
13
14const 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'));
});
善后工作
此时可以再修改一个"package.json",去掉里面的"main"这项,再往"script"中加入"build"脚本: 1
2
3"scripts": {
"build": "gulp"
}
总结
需要注意的是,这个构建的Demo配置得比较简单,代码没有压缩、混淆,也没有去除注释,如果你在代码里面写了什么不够友善的注释、在单量之类的数据上用了随机数,小心被用户一眼就看出来了哦。
后记
这篇文章只是我作为一个前端菜鸟、浅尝辄止地学习了一下前端相关的部分知识后的一个总结,相信既不全面、也会有不少错误之处,发出来一是希望能给同样对前端技术感兴趣的同学一点参考,也是希望能接受到大家的指导。 前端技术仍在飞速发展,想要真正的做到有些理解和感悟,还得不断地学习与练习,学而时习之,不亦说乎?