前端工程化
0x01 概述
(1)工程化
-
工程化:遵循一定的标准和规范,通过工具来降本增效、保证质量的一种方法
-
工程化用于解决一些问题,包括:
- 传统语言或语法的弊端
- 无法使用模块化或组件化
- 重复的机械式工作
- 代码风格与质量的不统一
- 过于甚至整体依赖于后端服务及其接口支持(特指前端工程化)
-
工程化的核心在于对项目整体的规划与架构,而工具则是实现这个核心的方法
(2)前端工程化
-
在前端项目过程中工程化的应用方法:
graph LR 创建项目-->编码-->预览-->测试-->提交-->部署-->编码- 创建项目:使用脚手架工具自动创建统一的项目结构
- 编码:使用合适的工具格式化代码、校验代码、编译、构建、打包
- 预览:通过现代化的 Web Server 提供热更新的预览方法
- 测试:通过 Mock 的方法模拟真实的后端接口,以及通过 Source Map 定位报错代码的位置
- 提交:使用 Git Hooks 在提交前对项目进行整体的检查
- 部署:自动化地实现持续集成、持续交付与持续部署(CI / CD)并自动发布
-
NodeJS 极大低促进了前端工程化的发展,vue-cli、create-react-app 等工具则是在此基础上建立的工程化集成工具
0x02 脚手架工具
(1)概述
- 脚手架工具:自动创建项目的基础结构、代码规范与约定的工具
- 包括:组织结构、开发范式、模块依赖、工具配置、基础代码
- 脚手架工具可以大概分为:
- 创建项目时
- 仅面向特定的框架,如 vue-cli、create-react-app 等
- 通用型,如 Yeoman
- 在开发过程中创建特定类型的文件,如 Plop
- 创建项目时
(2)Yeoman
- 官网链接:https://yeoman.io/
- 用于创建现代化 Web 应用的脚手架工具
- 可以通过 Yeoman 搭配不同的生成器来创建任何类型的项目
- 如 Web 应用、Chrome 插件、Node 模块等
a. 基础使用
需要预先准备好 NodeJS 以及 npm
- 以下使用 Node 20.15.0 与 npm 10.7.0
- 使用命令
npm install -g yo
全局安装 Yeoman 4.3.1 - 使用命令
npm install -g generator-node
全局安装 Node 模块生成器 2.8.0 - 在自定义目录下,使用命令
yo node
通过 Yeoman 创建一个 Node 模块
b. 子生成器
子生成器用于在父生成器及其项目目录下生成特定的文件
- 在创建好的 Node 模块目录下,使用命令
yo node:cli
生成 cli 应用所需要的文件,使该模块成为 cli 应用- 其中 cli 就是在 node-generator 中的子生成器
- 使用命令
npm install
安装所有模块所需的依赖 - 使用命令
npm link
将该模块作为命令在全局使用 - 使用命令
[模块名称] --help
查看自定义模块的命令帮助
c. 自定义生成器
-
创建生成器实际上就是创建一个 npm 的模块,基本结构为:
graph TB 1["generator-[name]"]-->generators & package.json generators-->app & sub app-->a[index.js] sub-->s[index.js]- generator-[name]:生成器模块根目录,也是生成器的名称,其中
name
自定义 - generators:生成器目录
- app:默认生成器目录
- sub:子生成器目录
- index.js:生成器实现
- package.json:模块包配置文件
- generator-[name]:生成器模块根目录,也是生成器的名称,其中
-
创建生成器步骤:
-
新建一个生成器根目录,命名为 generator-srigt
-
使用命令
npm init -y
快速创建一个 NodeJS 环境 -
使用命令
npm install yeoman-generator
安装 Yeoman 提供的生成器模块基类 -
在根目录下,按上述基本结构创建 generator 目录、app 目录、index.js 文件
-
在 index.js 编写一个简易的生成器
import Generator from "yeoman-generator"; export default class extends Generator { writing() { this.fs.write( this.destinationPath("index.html"), // 生成文件名称及其路径 "<!DOCTYPE html>\n<html>\n<head>\n<title>Hello World!</title>\n</head>\n<body>\n<h1>Hello World!</h1>\n<p>I'm running on port 3000.</p>\n</body>\n</html>" // 生成文件的内容 ); } }
并修改 package.json
{ // ... "type": "module", // ... }
-
使用命令
npm link
将该生成器作为命令在全局使用 -
在另外的目录下,使用命令
yo srigt
应用生成器 srigt
-
-
构建模板创建文件
-
在 app 目录下新建 templates 目录
-
在 templates 目录下新建模板 page.ejs
<!DOCTYPE html> <html> <head> <title> <%= title %> </title> </head> <body> <h1>Welcome, <%= name %>!</h1> </body> </html>
-
修改 app/index.js
import Generator from "yeoman-generator"; export default class extends Generator { writing() { this.fs.copyTpl( this.templatePath("page.ejs"), // 模板文件名称及其路径 this.destinationPath("page.html"), // 生成文件名称及其路径 { title: "Page Template", name: "SRIGT", } // 模板变量 ); } }
-
删除之前的项目文件,重新使用命令
yo srigt
应用生成器 srigt
-
-
接收用户输入
用户通过命令行参数实现输入
修改 app/index.js
import Generator from "yeoman-generator"; export default class extends Generator { prompting() { return this.prompt([ { type: "input", // 输入类型 name: "name", // 该输入项的名称 message: "Your project name", // 提示信息 default: this.appname, // 默认使用当前目录名称 }, ]).then((answers) => { this.answers = answers; // 将用户输入保存到 answers 属性 }); } writing() { this.fs.copyTpl( this.templatePath("page.ejs"), this.destinationPath("page.html"), { title: "Page Template", name: this.answers.name, // 调用 answers 属性中的值 } ); } }
-
发布生成器
-
新建 .gitignore 文件并写入
/node_modules
-
使用命令
git init
创建 Git 仓库 -
使用命令
git add .
将项目文件暂存到 Git 仓库 -
使用命令
git commit -m "init"
将暂存区的文件提交到 Git 仓库 -
通过 GitHub 或 Gitee 将 Git 仓库保存在远端
-
使用命令
npm publish
发布生成器模块- 注意:淘宝等镜像源是只读,需要修改为官方的镜像源才能正常发布
-
(3)Plop
-
官网链接:https://plopjs.com/
-
Plop 主要用于在已有项目中,自动创建相同结构的目录和文件及其内容
-
Plop 的使用方法:
-
使用命令
npm install --save-dev plop
安装用于非生产环境的 Plop -
在项目根目录下创建 plop_templates 目录,其中创建基于 Handlebars 的模板文件 component.hbs
export default () => { return ( <div> <h1>{{ name }} Component</h1> </div> ); };
-
在项目根目录下创建 plopfile.js
// Plop 入口文件 export default (plop) => { plop.setGenerator("component", { description: "Create a new component", prompts: [ { type: "input", name: "name", message: "What is your component name?", default: "MyComponent", }, ], actions: [ { type: "add", path: "src/components/{{pascalCase name}}/index.jsx", templateFile: "plop_templates/component.hbs", }, ], }); };
actions
项的值是数组,表示可以创建多个文件 -
在项目根目录下,使用命令
npm plop component
根据模板创建文件
-
(4)自定义脚手架工具
-
创建 srigt-cli 目录,其中使用命令
npm init -y
快速创建 NodeJS 环境 -
在 srigt-cli 目录下创建 index.js
#!/usr/bin/env node // 以上是 Node CLI 应用入口文件必要的文件头 console.log("hello world");
-
使用命令
npm link
将该脚手架作为命令在全局使用Windows 系统中,需要以管理员身份运行上述命令
-
使用命令
srigt-cli
启动该脚手架,效果为在终端输出hello world
-
返回脚手架项目目录,使用命令
npm install inquirer
安装用于接收用户输入的依赖 -
修改 index.js
#!/usr/bin/env node import inquirer from "inquirer"; inquirer .prompt([ { type: "input", name: "name", message: "Your project name", }, ]) .then((answers) => { console.log(answers); });
并修改 package.json
{ // ... "type": "module", // ... }
-
验证成功后,使用命令
npm install ejs
安装 EJS 模板引擎 -
在根目录下新建 templates 目录,并创建以下模板文件:
index.html
<!DOCTYPE html> <html> <head> <title>Document</title> <link rel="stylesheet" href="./style.css"> </head> <body> <h1>This project is called <%= name %>.</h1> </body> </html>
style.css
body { display: flex; flex-direction: column; align-items: center; justify-content: center; }
-
修改 index.js
#!/usr/bin/env node import ejs from "ejs"; import fs from "fs"; import inquirer from "inquirer"; import path from "path"; import { fileURLToPath } from "url"; inquirer .prompt([ { type: "input", name: "name", message: "Your project name", }, ]) .then((answers) => { const templatePath = path.join( path.dirname(fileURLToPath(import.meta.url)), "templates" ); // 模板目录 const destinationPath = process.cwd(); // 目标目录 // 读取模板目录下的所有文件 fs.readdir(templatePath, (err, files) => { if (err) { console.error("Error reading template directory:", err); return; } // 遍历文件 files.forEach((file) => { // 使用 EJS 模板引擎渲染 ejs.renderFile( path.join(templatePath, file), answers, (err, result) => { if (err) { console.error("Error rendering template:", err); return; } // 将渲染后的结果写入目标目录 fs.writeFileSync(path.join(destinationPath, file), result); } ); }); }); });
0x03 自动化构建
(1)概述
-
定义
- 自动化:通过机器代替人工
- 构建:把一个东西转换为另一个东西
- 自动化构建:将源代码自动转换为生产代码或程序
-
主要用于解决项目代码脱离运行环境兼容带来的问题
-
常用的自动化构架工具包括:
Webpack 属于模块打包文件,并不包含在此
Grunt Gulp FIS 构建方式 基于临时文件 基于内存 基于文件对象 构建速度 慢 较快 快速 其他特点 最早的自动化构建工具
插件生态完善支持同时执行多个任务
使用方式简单易懂将项目中的典型详情集中在内部
大而全适用情况 灵活多变 灵活多变 初学者
(2)Grunt
- 官网链接:https://gruntjs.com/
a. 基础使用
-
使用命令
npm init -y
快速创建 NodeJS 环境 -
使用命令
npm install grunt
安装 Grunt -
在项目根目录下创建 gruntfile.js
// Grunt 入口文件 module.exports = (grunt) => { grunt.registerTask("default", () => { console.log("hello world"); }); };
-
使用命令
npm exec grunt default
启动 Grunt 构建工具并执行名为default
的任务
由于default
也是默认任务,因此可以使用命令npm exec grunt
执行default
任务 -
修改 gruntfile.js,加入异步任务
module.exports = (grunt) => { grunt.registerTask("sync", () => { console.log("hello world from grunt-sync"); }); grunt.registerTask("async", function () { const done = this.async(); setTimeout(() => { console.log("hello world from grunt-async"); done(); }, 1000); }); grunt.registerTask("default", ["async", "sync"]); };
-
使用命令
npm exec grunt
执行default
任务
效果为先完成sync1
,再完成async
异步任务,后完成sync2
任务
b. 标记任务失败
-
修改 gruntfile.js,标记同步任务失败
grunt.registerTask("sync", () => { console.log("hello world from grunt-sync"); return false; });
-
标记异步任务失败
grunt.registerTask("async", function () { const done = this.async(); setTimeout(() => { console.log("hello world from grunt-async"); done(false); }, 1000); });
-
使用命令
npm exec grunt --force
构建时跳过报错继续构建构建过程中,遇到失败任务会停止后续构建,在命令中使用
--force
可以无视失败任务继续构建
c. 配置方法
-
Grunt 支持通过配置方法
initConfig
添加配置选项 API -
举例:读取 package.json
module.exports = (grunt) => { grunt.initConfig({ pkg: grunt.file.readJSON("package.json"), }); grunt.registerTask("default", () => { console.log(grunt.config("pkg")); console.log(grunt.config("pkg.name")); }); };
d. 多目标任务模式
多目标任务模式:让任务根据配置形成多个子任务
-
修改 gruntfile.js,添加多目标任务
build
module.exports = (grunt) => { grunt.registerMultiTask("build", function () { console.log("hello world"); }); grunt.registerTask("default", ["build"]); };
-
在配置方法中,添加任务同名的对象,其中包含多目标
module.exports = (grunt) => { grunt.initConfig({ build: { one: "001", two: "002", }, }); grunt.registerMultiTask("build", function () { console.log(`target: ${this.target}, data: ${this.data}`); }); grunt.registerTask("default", ["build"]); };
-
使用命令
npm exec grunt
-
多目标对象中的
options
属性会被作为配置选项,而非子任务module.exports = (grunt) => { grunt.initConfig({ build: { options: { zero: "000", }, one: "001", two: "002", }, }); grunt.registerMultiTask("build", function () { console.log("options:", this.options()); console.log(`target: ${this.target}, data: ${this.data}`); }); grunt.registerTask("default", ["build"]); };
-
使用命令
npm exec grunt
e. 插件使用
以自动清除临时文件插件为例
-
使用命令
npm install grunt-contrib-clean
-
修改 gruntfile.js,导入并配置
module.exports = (grunt) => { grunt.initConfig({ clean: { temp: "temp/**", }, }); grunt.loadNpmTasks("grunt-contrib-clean"); };
(3)Gulp
- 官网链接:https://gulpjs.com/
a. 基础使用
-
使用命令
npm init -y
快速创建 NodeJS 环境 -
使用命令
npm install --save-dev gulp
安装 Gulp -
在项目根目录下创建 gulpfile.js
// Gulp 入口文件 module.exports = { default: (done) => { console.log("hello world"); done(); }, };
最新的 Gulp (5.0.0)取消了同步任务模式,而是全部采用异步任务模式,需要通过回调函数等方式标记任务完成
-
使用命令
npm exec gulp default
启动 Grunt 构建工具并执行名为default
的任务
由于default
也是默认任务,因此可以使用命令npm exec gulp
执行default
任务
b. 组合任务
组合任务包括串行任务和并行任务
-
修改 gulpfile.js,创建多个任务
const task1 = (done) => { setTimeout(() => { console.log("task1"); done(); }, 1000); }; const task2 = (done) => { setTimeout(() => { console.log("task2"); done(); }, 1500); }; const task3 = (done) => { setTimeout(() => { console.log("task3"); done(); }, 750); };
-
使用
series
组合串行任务const { series } = require("gulp"); // ... module.exports = { default: series(task1, task2, task3), };
执行顺序为 1 2 3
-
使用
parallel
组合并行任务const { parallel } = require("gulp"); // ... module.exports = { default: parallel(task1, task2, task3), };
执行顺序为 3 1 2
c. 异步任务
-
回调函数
const task = (done) => { console.log("task"); done(); }; const task_error = (done) => { console.log("task_error"); done(new Error("error")); };
-
Promise
const task = () => { console.log("task"); return Promise.resolve(); }; const task_error = () => { console.log("task_error"); return Promise.reject(new Error("error")); };
async
/await
+ Promiseconst task = async () => { console.log("task"); await new Promise((resolve) => { setTimeout(resolve, 1); }); }; const task_error = async () => { console.log("task_error"); await new Promise((_, reject) => { setTimeout(() => reject(new Error("error")), 1); }); };
-
流(以读写文件流为例)
const task = async () => { const readStream = fs.createReadStream("package.json"); const writeStream = fs.createWriteStream("temp.txt"); readStream.pipe(writeStream); return readStream; }; // or const task = async (done) => { const readStream = fs.createReadStream("package.json"); const writeStream = fs.createWriteStream("temp.txt"); readStream.pipe(writeStream); readStream.on("end", () => { done(); }); };
d. 构建过程核心原理
构建过程:
graph LR a[输入<br/>读取流]-->b[加工<br/>转换流]-->c[输出<br/>写入流]以 CSS 文件压缩为例
-
创建一个样式文件 style.css
body { margin: 0; padding: 0; box-sizing: border-box; min-height: 100vh; background-color: #fafafa; color: #333; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1rem; } .container { width: 100%; max-width: 400px; background-color: #fff; padding: 2rem; border-radius: 5px; }
-
修改 gulpfile.js
const fs = require("fs"); const { Transform } = require("stream"); module.exports = { default: () => { // 读取流 const readStream = fs.createReadStream("style.css"); // 写入流 const writeStream = fs.createWriteStream("style.min.css"); // 转换流 const transform = new Transform({ transform: (chunk, encoding, callback) => { const input = chunk.toString(); const output = input.replace(/\s+/g, "").replace(/\/\*.+?\*\//g, ""); callback(null, output); }, }); // 链接流 readStream.pipe(transform).pipe(writeStream); return readStream; }, };
e. 插件使用
-
以 CSS 文件压缩为例
-
使用命令
npm install --save-dev gulp-clean-css gulp-rename
安装相关插件 -
修改 gulpfile.js
const { src, dest } = require("gulp"); const cleanCss = require("gulp-clean-css"); const rename = require("gulp-rename"); module.exports = { default: () => src("style.css") .pipe(cleanCss()) .pipe(rename({ extname: ".min.css" })) .pipe(dest(".")), };
-
-
常用插件:
名称 功能 gulp-load-plugins 自动加载插件 del 清除文件 browser-sync 热更新服务器 gulp-if 添加条件判断到转换流 gulp-swig 编译页面 gulp-htmlmin 压缩页面 gulp-sass 编译 SASS gulp-clean-css 压缩 CSS gulp-babel 编译脚本 gulp-nglify 压缩脚本 gulp-imagemin 处理图片和字体 gulp-useref 使用依赖
(4)FIS
a. 基础使用
-
使用命令
npm install -g fis3
全局安装 FIS -
在项目根目录下创建 fis-conf.js,实现资源定位
// FIS 配置文件 fis.match('*.{js}', { release: "/assets/$0" });
-
在已有项目目录下,使用命令
fis3 release -d dist
将 FIS 构建输出到当前目录下的 dist 目录中
b. 插件使用
以编译 SASS 文件为例
-
使用命令
npm install -g fis-parser-node-sass
全局安装 SASS 编译器 -
修改 fis-conf.js
fis.match("**/*.scss", { rExt: ".css", parser: fis.plugin("node-sass"); });
-
使用命令
fis3 release -d dist
发布
-end-