前端项目工程化
# 前端工程的三个阶段
现在的前端开发倒也并非一无所有,回顾一下曾经经历过或听闻过的项目,为了提升其前端开发效率和运行性能,前端团队的工程建设大致会经历三个阶段:
# 库 / 框架选型
前端工程建设的第一项任务就是根据项目特征进行技术选型。
基本上现在没有人完全从 0 开始做网站,哪怕是政府项目用个 jQuery 都很正常吧,React/Angularjs 等框架横空出世,解放了不少生产力,合理的技术选型可以为项目节省许多工程量这点毋庸置疑。
# 简单构建优化
选型之后基本上就可以开始敲码了,不过光解决开发效率还不够,必须要兼顾运行性能。前端工程进行到第二阶段会选型一种构建工具,对代码进行压缩,校验,之后再以页面为单位进行简单的资源合并。
# JS/CSS 模块化开发
分而治之是软件工程中的重要思想,是复杂系统开发和维护的基石,这点放在前端开发中同样适用。在解决了基本开发效率运行效率问题之后,前端团队开始思考维护效率,模块化是目前前端最流行的分治手段。
JS 模块化方案很多,AMD/CommonJS/UMD/ES6 Module 等,对应的框架和工具也一大堆,CSS 模块化开发基本都是在 less、sass、stylus 等预处理器的 import/mixin 特性支持下实现的。
# 组件化开发
前端是一种技术问题较少、工程问题较多的软件开发领域。
当我们要开发一款完整的 Web 应用时,前端将面临更多的工程问题,比如:
大体量:多功能、多页面、多状态、多系统;
大规模:多人甚至多团队合作开发;
高性能:CDN 部署、缓存控制、文件指纹、缓存复用、请求合并、按需加载、同步 / 异步加载、移动端首屏 CSS 内嵌、HTTP 2.0 服务端资源推送。
这些无疑是一系列严肃的系统工程问题。
分治的确是非常重要的工程优化手段。前端作为一种 GUI 软件,光有 JS/CSS 的模块化还不够,对于 UI 组件的分治也有着同样迫切的需求:
如上图,这是前端组件化开发理念,简单解读一下:
页面上的每个 独立的 可视 / 可交互区域视为一个组件;
每个组件对应一个工程目录,组件所需的各种资源都在这个目录下就近维护;
由于组件具有独立性,因此组件与组件之间可以 自由组合;
页面只不过是组件的容器,负责组合组件形成功能完整的界面;
当不需要某个组件,或者想要替换组件时,可以整个目录删除 / 替换。
其中第二项描述的就近维护原则,它为前端开发提供了很好的分治策略,每个开发者都将清楚的知道,自己所开发维护的功能单元,其代码必然存在于对应的组件目录中,在那个目录下能找到有关这个功能单元的所有内部逻辑,样式也好,JS 也好,页面结构也好,都在那里。
组件化开发具有较高的通用性,无论是前端渲染的单页面应用,还是后端模板渲染的多页面应用,组件化开发的概念都能适用。组件 HTML 部分根据业务选型的不同,可以是静态的 HTML 文件,可以是前端模板,也可以是后端模板:
不同的技术选型决定了不同的组件封装和调用策略。
基于这样的工程理念,我们很容易将系统以独立的组件为单元进行分工划分:
由于系统功能被分治到独立的模块或组件中,粒度比较精细,组织形式松散,开发者之间不会产生开发时序的依赖,大幅提升并行的开发效率,理论上允许随时加入新成员认领组件开发或维护工作,也更容易支持多个团队共同维护一个大型站点的开发。
结合前面提到的模块化开发,整个前端项目可以划分为这么几种开发概念:
名称 | 说明 | 举例 |
---|---|---|
JS 模块 | 独立的算法和数据单元 | 浏览器环境检测 (detect),网络请求 (ajax),应用配置 (config),DOM 操作 (dom),工具函数 (utils),以及组件里的 JS 单元 |
CSS 模块 | 独立的功能性样式单元 | 栅格系统 (grid),字体图标 (icon-fonts),动画样式 (animate),以及组件里的 CSS 单元 |
UI 组件 | 独立的可视 / 可交互功能单元 | 页头 (header),页尾 (footer),导航栏 (nav),搜索框 (search) |
页面 | 前端这种 GUI 软件的界面状态,是 UI 组件的容器 | 首页 (index),列表页 (list),用户管理 (user) |
应用 | 整个项目或整个站点被称之为应用,由多个页面组成 |
以上 5 种开发概念以相对较少的规则组成了前端开发的基本工程结构,基于这些理念,前端开发就成了这个样子:
示意图 | 描述 |
---|---|
![]() | 整个 Web 应用由页面组成 |
![]() | 页面由组件组成 |
![]() | 一个组件一个目录,资源就近维护 |
![]() | 组件可组合,组件的 JS 可依赖其他 JS 模块,CSS 可依赖其他 CSS 单元 |
综合上面的描述,对于一般中小规模的项目,大致可以规划出这样的源码目录结构:
如果项目规模较大,涉及多个团队协作,还可以将具有相关业务功能的页面组织在一起,形成一个子系统,进一步将整个站点拆分出多个子系统来分配给不同团队维护
# NPM 包管理器
NPM 全称 node package manger,是 Node 的开放式模块登记和管理系统。
官网:https://www.npmjs.cn/
仓库:https://www.npmjs.com/package/repository
中文参考:https://www.axihe.com/api/npm/api/api.html
# NPM 能干什么
社区的力量,程序员自古以来就有社区文化。加入社区最大的好处之一是,你可以使用别人贡献的代码,你也可以贡献代码给别人用。
社区的意思是:拥有共同职业或兴趣的人们,自发组织在一起,通过分享信息和资源进行合作。虚拟社区的参与者经常会在线讨论相关话题,或访问某些网站。
在 GitHub 还没有兴起的年代,前端是通过网址来共享代码。现在依然没有完全废弃。NPM 最核心的任务就是共享代码
NPM 的思路大概是这样的:
买个服务器作为代码仓库(registry),在里面放所有需要被共享的代码
发邮件通知 jQuery、Bootstrap、Underscore 作者使用 npm publish 把代码提交到 registry 上
社区里的其他人如果想使用这些代码,就把 jquery、bootstrap 和 underscore 写到 package.json 里,执行对应的命令下载这些代码
下载完的代码出现在 node_modules 目录里,可以随意使用了。这些可以被使用的代码被叫做"包"(package)
# NPM 基本使用
由于新版的 nodejs 已经集成了 npm,所以之前 npm 也一并安装好了。同样可以通过输入 "npm -v" 来测试是否成功安装。
npm init
创建一个 package.json 文件
命令语法
npm init [--force|-f|--yes|-y|--scope]
npm init <@scope> (same as `npx <@scope>/create`)
npm init [<@scope>/]<name> (same as `npx [<@scope>/]create-<name>`)
2
3
目录说明
./demo1/
|-- node_modules # npm 包存放的文件夹
|--.npmrc # npm 配置文件
|-- package-lock.json # npm 锁定文件的说明
|-- package.json # npm 的 package
2
3
4
5
# .npmrc
npm 从命令行,环境变量和 npmrc 文件获取其配置设置
npm config 命令可用于更新和编辑用户和全局 .npmrc 文件的内容
.npmrc 当文件中的行以;或#字符开头时,它们被解释为注释。
存放位置
每个项目的配置文件(/path/to/my/project/.npmrc): 存在于项目根目录下的.npmrc 配置文件
每个用户的配置文件(~/.npmrc):$HOME/.npmrc 存在于用户根目录下
全局配置文件( PREFIX/etc/.npmrc
npm 内置配置文件( /path/to/npm/npmrc ):这是一个不可更改的"内置"配置文件,npm 在更新之间保持一致。
; 指定默认的编辑器
editor=C:\Tools\SublimeTextBuild3207x64\sublime_text.exe
; 模块库
registry=https://registry.npm.taobao.org
; 模块安装地址
prefix=C:\nodejs-moudules\moudules
; 临时文件地址
tmp=C:\nodejs-moudules\tmp
; 缓存地址
cache=C:\nodejs-moudules\npm_cache
2
3
4
5
6
7
8
9
10
npm config
该命令可用于查看、编辑用户和全局 npmrc 文件的内容。
命令语法
npm config set <key> <value> [-g|--global]
npm config get <key>
npm config delete <key>
npm config list [-l] [--json]
npm config edit
npm get <key>
npm set <key> <value> [-g|--global]
2
3
4
5
6
7
# 安装模块
命令语法
npm install (with no args, in package dir)
npm install [<@scope>/]<name>
npm install [<@scope>/]<name>@<tag>
npm install [<@scope>/]<name>@<version>
npm install [<@scope>/]<name>@<version range>
npm install <git-host>:<git-user>/<repo-name>
npm install <git repo url>
npm install <tarball file>
npm install <tarball url>
npm install <folder>
2
3
4
5
6
7
8
9
10
默认将所有指定的软件包保存到其中。此外,您可以使用一些其他标志来控制在何处以及如何保存它们:
- -D, –save-dev:包将出现在您的中 devDependencies。
局部安装
将安装包放在 ./node_modules 下(运行 npm 命令时所在的目录),如果没有 node_modules 目录,会在当前执行 npm 命令的目录下生成 node_modules 目录。
可以通过 require() 来引入本地安装的包。
全局安装
将安装包放在 /usr/local 下或者你 node 的安装目录。
可以直接在命令行里使用。
npm install express -g
# 卸载模块
我们可以使用以下命令来卸载 Node.js 模块。
npm uninstall express
npm cache clean
2
3
# 更新模块
我们可以使用以下命令更新模块:
npm update express
# Webpack 基础
Webpack 是基于 nodejs 实现的, Node.js 是前端工程化的重要支柱之一,所以想使用 Webpack 构建项目,首先需要做的是理清楚 NPM 、NodeJS 以及 Webpack 他们之间的关系
NodeJS 是 JavaScript 编写服务器端代码的一个运行环境,类似于 Java 的 JDK
NPM 是 NodeJS 的一个功能模块,主要用于安装依赖
Webpack 是基于 NodeJS 实现的一个软件,所以它里面所有功能 (loader、plugins) 都是通过 NPM 进行安装的
NPM 的服务器在国外,因为防火墙的原因,所以下载东西速度会很慢,CNPM 是淘宝提供的 NPM 国内版本,服务器在国内
yarn 和 npm 一样,也是基于 NodeJS 实现的一个依赖管理工具,因为内部机制不同所以只是下载速度和信息提示相对 npm 来说更友好一点,个人不建议使用 yarn
本质上,Webpack 是一个现代 JavaScript 应用程序的静态模块打包器 (module bundler)。当 Webpack 处理应用程序时,它会递归地构建一个依赖关系图 (dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
从 Webpack v4.0.0 开始,可以不用引入一个配置文件。然而,Webpack 仍然还是高度可配置的。在开始前你需要先理解四个核心概念
# 入口 (entry)
入口起点 (entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,Webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
# 输出 (output)
output 属性告诉 Webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。你可以通过在配置中指定一个 output 字段,来配置这些处理过程
# loader
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
# 插件 (plugins)
loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。
想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项 (option) 自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建它的一个实例。
参考地址列表:
npm:https://www.npmjs.com.cn/getting-started/what-is-npm
cnpm:https://npm.taobao.org
yarn:https://yarn.bootcss.com
webpack:https://www.webpackjs.com
NodeJS:http://nodejs.cn
# 案例 -HelloWrold
官方文档:https://www.webpackjs.com/guides
# 创建项目
npm init -y
# 安装 webpack
npm install webpack --save-dev
npm install webpack webpack-cli --save-dev
2
3
4
5
6
完整的 package.json 内容如下:
{
"name": "demo1",
"version": "1.0.0",
"description": "",
"scripts": {
"build": " webpack --config webpack.config.js --progress --colors ",
"dev": "webpack-dev-server --inline --colors --progress"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^4.30.0",
"webpack-cli": "^3.3.2",
"webpack-dev-server": "^3.3.1"
},
"dependencies": {
"jquery": "^3.4.1"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
核心内容说明:
name:用于标明项目名称,该属性必须存在,同时要求名称中不能出现大写字母
version:用于标明项目的版本号,该属性必须存在,版本号一般有三部分组成:主版本。次版本。bug 修复版本
scripts:我们可以借助 npm 执行一些复杂的命令,而为了避免每次都输入这些复杂的命令,可以借助该属性将这些复杂的命令做成小脚本,运行其中某个脚本的命令如下:
npm run 脚本名称
例如:npm run dev
devDependencies:用于记录在开发环境中依赖的版本和名称,使用
npm i jquery --save-dev
就可以把依赖信息写入。在新版本的 npm 中已经可以使用 -D 来代替 --save-dev,完整的例子:npm i jquery -D
Dependencies:用于记录在生产环境中依赖的版本和名称。使用
npm i jquery --save
就可以把依赖信息写入
现在我们将创建以下目录结构、文件和内容:
webpack-demo
|- package.json
|- index.html
|- /src
|- index.js
2
3
4
5
src/index.js
function component() {
var element = document.createElement('div');
// Lodash(目前通过一个 script 脚本引入)对于执行这一行是必需的
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
2
3
4
5
6
7
8
9
10
11
12
index.html
<!doctype html>
<html>
<head>
<title>起步</title>
<script src="https://unpkg.com/lodash@4.16.6"></script>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
使用这种方式去管理 JavaScript 项目会有一些问题:
无法立即体现,脚本的执行依赖于外部扩展库 (external library)。
如果依赖不存在,或者引入顺序错误,应用程序将无法正常运行。
如果依赖被引入但是并没有使用,浏览器将被迫下载无用代码。
调整目录结构,让 webpack 来管理这些脚本
webpack-demo
|- package.json
|- /dist
|- index.html
|- /src
|- index.js
2
3
4
5
6
7
# 安装 lodash
npm install --save lodash
2
3
src/index.js
import _ from 'lodash';
function component() {
var element = document.createElement('div');
// Lodash(目前通过一个 script 脚本引入)对于执行这一行是必需的
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
2
3
4
5
6
7
8
9
10
11
12
dist/index.html
<!doctype html>
<html>
<head>
<title>起步</title>
</head>
<body>
<script src="main.js"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
执行 npx webpack,会将我们的脚本作为入口起点,然后 输出 为 main.js