前端开发史

历史

刚接触前端的时候,做一个页面,是先创建 HTML 页面文件写页面结构,再在里面写 CSS 代码美化页面,再根据需要写一些 JavaScript 代码增加交互功能,需要几个页面就创建几个页面。
而实际上的前端开发工作与以前的前端开发已经完全不同了,早已进入了前端工程化开发的时代,已经充满了各种现代化框架、预处理器、代码编译…
最终的产物也不再单纯是多个 HTML 页面,经常能看到 SPA / SSR / SSG 等词汇的身影(现代化的开发概念)。

传统开发的弊端

  1. 多个文件中可能存在同名的变量声明,引起变量冲突
  2. 引入多个资源文件时,比如有多个 JS 文件,在其中一个 JS 文件里面使用了在别处声明的变量,无法快速找到是在哪里声明的,大型项目难以维护
  3. 类似第 1 、 2 点提到的问题无法轻松预先感知,很依赖开发人员人工定位原因
  4. 大部分代码缺乏分割,比如一个工具函数库,很多时候需要整包引入到 HTML 里,文件很大,然而实际上只需要用到其中一两个方法
  5. 由第 4 点大文件延伸出的问题, script 的加载从上到下,容易阻塞页面渲染
  6. 不同页面的资源引用都需要手动管理,容易造成依赖混乱,难以维护
  7. 如果要压缩 CSS 、混淆 JS 代码,也是要人力操作使用工具去一个个处理后替换,容易出错

前端工程化

工程化带来的优势

为了解决传统开发的弊端,前端也开始引入工程化开发的概念,借助工具来解决人工层面的烦琐事情。

开发层面的优势

在开发层面,前端工程化有以下这些好处:

  1. 引入了模块化和包的概念,作用域隔离,解决了代码冲突的问题
  2. 按需导出和导入机制,让编码过程更容易定位问题
  3. 自动化的代码检测流程,有问题的代码在开发过程中就可以被发现
  4. 编译打包机制可以让使用开发效率更高的编码方式,比如 Vue 组件、 CSS 的各种预处理器
  5. 引入了代码兼容处理的方案( e.g. Babel ),可以让开发者自由使用更先进的 JavaScript 语句,而无需顾忌浏览器兼容性,因为最终会转换为浏览器兼容的实现版本
  6. 引入了 Tree Shaking 机制,清理没有用到的代码,减少项目构建后的体积
    还有非常多的体验提升,列举不完。而对应的工具,根据用途也会有非常多的选择,在后面的学习过程中,会一步一步体验到工程化带来的好处。

团队协作的优势

统一的项目结构(脚手架的升级与配置 )

以前的项目结构比较看写代码的人的喜好,虽然一般在研发部门里都有 “团队规范” 这种东西,但靠自觉性去配合的事情,还是比较难做到统一,特别是项目很赶的时候。

工程化后的项目结构非常清晰和统一,以 Vue 项目来说,通过脚手架创建一个新项目之后,它除了提供能直接运行 Hello World 的基础代码之外,还具备了如下的统一目录结构:

  • src 是源码目录
  • src/main.ts 是入口文件
  • src/views 是路由组件目录
  • src/components 是子组件目录
  • src/router 是路由目录

虽然也可以自行调整成别的结构,但根据笔者在多年的工作实际接触下来,以及从很多开源项目的代码里看到的,都是沿用脚手架创建的项目结构(不同脚手架创建的结构会有所不同,但基于同一技术栈的项目基本上都具备相同的结构)。

统一的代码风格(添加协作规范)

不管是接手其他人的代码或者是修改自己不同时期的代码,可能都会遇到这样的情况,例如一个模板语句,上面包含了很多属性,有的人喜欢写成一行,属性多了维护起来很麻烦,需要花费较多时间辨认:

1
2
3
4
5
6
7
8
9
10
<template>
<div class="list">
<!-- 这个循环模板有很多属性 -->
<div class="item" :class="{ `top-${index + 1}`: index < 3 }" v-for="(item, index)
in list" :key="item.id" @click="handleClick(item.id)">
<span>{{ item.text }}</span>
</div>
<!-- 这个循环模板有很多属性 -->
</div>
</template>

而工程化配合统一的代码格式化规范,可以让不同人维护的代码,最终提交到 Git 上的时候,风格都保持一致,并且类似这种很多属性的地方,都会自动帮格式化为一个属性一行,维护起来就很方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div class="list">
<!-- 这个循环模板有很多属性 -->
<div
class="item"
:class="{ `top-${index + 1}`: index < 3 }"
v-for="(item, index) in list"
:key="item.id"
@click="handleClick(item.id)"
>
<span>{{ item.text }}</span>
</div>
<!-- 这个循环模板有很多属性 -->
</div>
</template>

同样的,写 JavaScript 时也会有诸如字符串用双引号还是单引号,缩进是 Tab 还是空格,如果用空格到底是要 4 个空格还是 2 个空格等一堆 “没有什么实际意义” 、但是不统一的话协作起来又很难受的问题……

在工程化项目这些问题都可以交给程序去处理,在书写代码的时候,开发者可以先按照自己的习惯书写,然后再执行命令进行格式化,或者是在提交代码的时候配合 Git Hooks 自动格式化,都可以做到统一风格。

可复用的模块和组件(依赖包和插件)

传统项目比较容易被复用的只有 JavaScript 代码和 CSS 代码,会抽离公共函数文件上传到 CDN ,然后在 HTML 页面里引入这些远程资源, HTML 代码部分通常只有由 JS 创建的比较小段的 DOM 结构。

并且通过 CDN 引入的资源,很多时候都是完整引入,可能有时候只需要用到里面的一两个功能,却要把很大的完整文件都引用进来。

这种情况下,在前端工程化里,就可以抽离成一个开箱即用的 npm 组件包,并且很多包都提供了模块化导出,配合构建工具的 Tree Shaking ,可以抽离用到的代码,没有用到的其他功能都会被抛弃,不会一起发布到生产环境。

代码健壮性有保障

传统的开发模式里,只能够写 JavaScript ,而在工程项目里,可以在开发环境编写带有类型系统的 TypeScript ,然后再编译为浏览器能认识的 JavaScript 。
在开发过程中,编译器会检查代码是否有问题,比如在 TypeScript 里声明了一个布尔值的变量,然后不小心将它赋值为数值:

1
2
3
4
5
// 声明一个布尔值变量
let bool: boolean = true

// 在 TypeScript ,不允许随意改变类型,这里会报错
bool = 3

编译器检测到这个行为的时候就会抛出错误:

1
2
3
4
5
6
7
8
9
# ...
return new TSError(diagnosticText, diagnosticCodes);
^
TSError: ⨯ Unable to compile TypeScript:
src/index.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'boolean'.

2 bool = 3
~~~~
# ...

从而得以及时发现问题并修复,减少线上事故的发生。

团队开发效率高

在前后端合作环节,可以提前 Mock 接口与后端工程师同步开发,如果遇到跨域等安全限制,也可以进行本地代理,不受跨域困扰。

前端工程在开发过程中,还有很多可以交给程序处理的环节,像前面提到的代码格式化、代码检查,还有在部署上线的时候也可以配合 CI/CD 完成自动化流水线,不像以前改个字都要找服务端工程师去更新,可以把非常多的人力操作剥离出来交给程序。

求职竞争上的优势

近几年前端开发领域的相关岗位,都会在招聘详情里出现类似的描述:

  1. 熟悉 Vue / React 等主流框架,对前端组件化和模块化有深入的理解和实践
  2. 熟悉面向组件的开发模式,熟悉 Webpack / Vite 等构建工具
  3. 熟练掌握微信小程序开发,熟悉 Taro 框架或 uni-app 框架优先
  4. 熟悉 Scss / Less / Stylus 等预处理器的使用
  5. 熟练掌握 TypeScript 者优先
  6. 有良好的代码风格,结构设计与程序架构者优先
  7. 了解或熟悉后端开发者优先(如 Java / Go / Node.js )

组件化开发、模块化开发、 Webpack / Vite 构建工具、 Node.js 开发… 这些技能都属于前端工程化开发的知识范畴,不仅在面试的时候会提问,入职后新人接触的项目通常也是直接指派前端工程化项目,如果能够提前掌握相关的知识点,对求职也是非常有帮助的!

Vue.js 与工程化

Vue 和 React 这些主流的前端框架,前端框架是前端工程化开发里面不可或缺的成员。
框架能够充分的利用前端工程化相关的领先技术,不仅在开发层面降低开发者的上手难度、提升项目开发效率,在构建出来的项目成果上也有着远比传统开发更优秀的用户体验。
结合 Vue.js 框架 3.0 系列的全新版本,从项目开发的角度,在帮助开发者入门前端工程化的同时,更快速的掌握一个流行框架的学习和使用。

了解 Vue.js 与全新的 3.0 版本

如果还没有体验过 Vue ,可以把以下代码复制到的代码编辑器,保存成一个 HTML 文件(例如: hello.html ),并在浏览器里打开访问,同时请唤起浏览器的控制台面板(例如 Chrome 浏览器是按 F12 或者鼠标右键点 “检查” ),在 Console 面板查看 Log 的打印。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<!-- 这是使用 Vue 实现的 demo -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello Vue</title>
<script src="https://unpkg.com/vue@3"></script>
</head>
<body>
<div id="app">
<!-- 通过 `{{ 变量名 }}` 语法渲染响应式变量 -->
<p>Hello {{ name }}!</p>

<!-- 通过 `v-model` 双向绑定响应式变量 -->
<!-- 通过 `@input` 给输入框绑定输入事件 -->
<input
type="text"
v-model="name"
placeholder="输入名称打招呼"
@input="printLog"
/>

<!-- 通过 `@click` 给按钮绑定点击事件 -->
<button @click="reset">重置</button>
</div>

<script>
const { createApp, ref } = Vue
createApp({
// `setup` 是一个生命周期钩子
setup() {
// 默认值
const DEFAULT_NAME = 'World'

// 用于双向绑定的响应式变量
const name = ref(DEFAULT_NAME)

// 打印响应式变量的值到控制台
function printLog() {
// `ref` 变量需要通过 `.value` 操作值
console.log(name.value)
}

// 重置响应式变量为默认值
function reset() {
name.value = DEFAULT_NAME
printLog()
}

// 需要 `return` 出去才可以被模板使用
return { name, printLog, reset }
},
}).mount('#app')
</script>
</body>
</html>

这是一个基于 Vue 3 组合式 API 语法的 demo ,它包含了两个主要功能:

可以在输入框修改输入内容,上方的 Hello World! 以及浏览器控制台的 Log 输出,都会随着输入框内容的变更而变化
可以点击 “重置” 按钮,响应式变量被重新赋值的时候,输入框的内容也会一起变化为新的值
这是 Vue 的特色之一:数据的双向绑定。

对比普通的 HTML 文件需要通过输入框的 oninput 事件手动编写视图的更新逻辑, Vue 的双向绑定功能大幅度减少了开发过程的编码量。

在未接触 Vue 这种编程方式之前,相信大部分人首先想到的是直接操作 DOM 来实现需求,为了更好的进行对比,接下来用原生 JavaScript 实现一次相同的功能:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!-- 这是使用原生 JavaScript 实现的 demo -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello World</title>
</head>
<body>
<div id="app">
<!-- 通过一个 `span` 标签来指定要渲染数据的位置 -->
<p>Hello <span id="name"></span>!</p>

<!-- 通过 `oninput` 给输入框绑定输入事件 -->
<input
id="input"
type="text"
placeholder="输入名称打招呼"
oninput="handleInput()"
/>

<!-- 通过 `onclick` 给按钮绑定点击事件 -->
<button onclick="reset()">重置</button>
</div>

<script>
// 默认值
const DEFAULT_NAME = 'World'

// 要操作的 DOM 元素
const nameElement = document.querySelector('#name')
const inputElement = document.querySelector('#input')

// 处理输入
function handleInput() {
const name = inputElement.value
nameElement.innerText = name
printLog()
}

// 打印输入框的值到控制台
function printLog() {
const name = inputElement.value
console.log(name)
}

// 重置 DOM 元素的文本和输入框的值
function reset() {
nameElement.innerText = DEFAULT_NAME
inputElement.value = DEFAULT_NAME
printLog()
}

// 执行一次初始化,赋予 DOM 元素默认文本和输入框的默认值
window.addEventListener('load', reset)
</script>
</body>
</html>