前端工程化

0x01 概述

(1)工程化

  • 工程化:遵循一定的标准和规范,通过工具来降本增效、保证质量的一种方法

  • 工程化用于解决一些问题,包括:

    1. 传统语言或语法的弊端
    2. 无法使用模块化或组件化
    3. 重复的机械式工作
    4. 代码风格与质量的不统一
    5. 过于甚至整体依赖于后端服务及其接口支持(特指前端工程化
  • 工程化的核心在于对项目整体的规划与架构,而工具则是实现这个核心的方法

(2)前端工程化

  • 在前端项目过程中工程化的应用方法:

    graph LR 创建项目-->编码-->预览-->测试-->提交-->部署-->编码
    1. 创建项目:使用脚手架工具自动创建统一的项目结构
    2. 编码:使用合适的工具格式化代码、校验代码、编译、构建、打包
    3. 预览:通过现代化的 Web Server 提供热更新的预览方法
    4. 测试:通过 Mock 的方法模拟真实的后端接口,以及通过 Source Map 定位报错代码的位置
    5. 提交:使用 Git Hooks 在提交前对项目进行整体的检查
    6. 部署:自动化地实现持续集成、持续交付与持续部署(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
  1. 使用命令 npm install -g yo 全局安装 Yeoman 4.3.1
  2. 使用命令 npm install -g generator-node 全局安装 Node 模块生成器 2.8.0
  3. 在自定义目录下,使用命令 yo node 通过 Yeoman 创建一个 Node 模块

b. 子生成器

子生成器用于在父生成器及其项目目录下生成特定的文件

  1. 在创建好的 Node 模块目录下,使用命令 yo node:cli 生成 cli 应用所需要的文件,使该模块成为 cli 应用
    • 其中 cli 就是在 node-generator 中的子生成器
  2. 使用命令 npm install 安装所有模块所需的依赖
  3. 使用命令 npm link 将该模块作为命令在全局使用
  4. 使用命令 [模块名称] --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:模块包配置文件
  • 创建生成器步骤:

    1. 新建一个生成器根目录,命名为 generator-srigt

    2. 使用命令 npm init -y 快速创建一个 NodeJS 环境

    3. 使用命令 npm install yeoman-generator 安装 Yeoman 提供的生成器模块基类

    4. 在根目录下,按上述基本结构创建 generator 目录、app 目录、index.js 文件

    5. 在 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",
        // ...
      }
      
    6. 使用命令 npm link 将该生成器作为命令在全局使用

    7. 在另外的目录下,使用命令 yo srigt 应用生成器 srigt

  • 构建模板创建文件

    1. 在 app 目录下新建 templates 目录

    2. 在 templates 目录下新建模板 page.ejs

      <!DOCTYPE html>
      <html>
      
      <head>
        <title>
          <%= title %>
        </title>
      </head>
      
      <body>
        <h1>Welcome, <%= name %>!</h1>
      </body>
      
      </html>
      
    3. 修改 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",
            } // 模板变量
          );
        }
      }
      
    4. 删除之前的项目文件,重新使用命令 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 属性中的值
          }
        );
      }
    }
    
  • 发布生成器

    1. 新建 .gitignore 文件并写入

      /node_modules
      
    2. 使用命令 git init 创建 Git 仓库

    3. 使用命令 git add . 将项目文件暂存到 Git 仓库

    4. 使用命令 git commit -m "init" 将暂存区的文件提交到 Git 仓库

    5. 通过 GitHub 或 Gitee 将 Git 仓库保存在远端

    6. 使用命令 npm publish 发布生成器模块

      • 注意:淘宝等镜像源是只读,需要修改为官方的镜像源才能正常发布

(3)Plop

  • 官网链接:https://plopjs.com/

  • Plop 主要用于在已有项目中,自动创建相同结构的目录和文件及其内容

  • Plop 的使用方法:

    1. 使用命令 npm install --save-dev plop 安装用于非生产环境的 Plop

    2. 在项目根目录下创建 plop_templates 目录,其中创建基于 Handlebars 的模板文件 component.hbs

      export default () => {
        return (
          <div>
            <h1>{{ name }} Component</h1>
          </div>
        );
      };
      
    3. 在项目根目录下创建 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 项的值是数组,表示可以创建多个文件

    4. 在项目根目录下,使用命令 npm plop component 根据模板创建文件

(4)自定义脚手架工具

  1. 创建 srigt-cli 目录,其中使用命令 npm init -y 快速创建 NodeJS 环境

  2. 在 srigt-cli 目录下创建 index.js

    #!/usr/bin/env node
    // 以上是 Node CLI 应用入口文件必要的文件头
    
    console.log("hello world");
    
  3. 使用命令 npm link 将该脚手架作为命令在全局使用

    Windows 系统中,需要以管理员身份运行上述命令

  4. 使用命令 srigt-cli 启动该脚手架,效果为在终端输出 hello world

  5. 返回脚手架项目目录,使用命令 npm install inquirer 安装用于接收用户输入的依赖

  6. 修改 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",
      // ...
    }
    
  7. 验证成功后,使用命令 npm install ejs 安装 EJS 模板引擎

  8. 在根目录下新建 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;
    }
    
  9. 修改 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

a. 基础使用

  1. 使用命令 npm init -y 快速创建 NodeJS 环境

  2. 使用命令 npm install grunt 安装 Grunt

  3. 在项目根目录下创建 gruntfile.js

    // Grunt 入口文件
    
    module.exports = (grunt) => {
      grunt.registerTask("default", () => {
        console.log("hello world");
      });
    };
    
  4. 使用命令 npm exec grunt default 启动 Grunt 构建工具并执行名为 default 的任务
    由于 default 也是默认任务,因此可以使用命令 npm exec grunt 执行 default 任务

  5. 修改 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"]);
    };
    
  6. 使用命令 npm exec grunt 执行 default 任务
    效果为先完成 sync1,再完成 async 异步任务,后完成 sync2 任务

b. 标记任务失败

  1. 修改 gruntfile.js,标记同步任务失败

    grunt.registerTask("sync", () => {
      console.log("hello world from grunt-sync");
      return false;
    });
    
  2. 标记异步任务失败

    grunt.registerTask("async", function () {
      const done = this.async();
      setTimeout(() => {
        console.log("hello world from grunt-async");
        done(false);
      }, 1000);
    });
    
  3. 使用命令 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. 多目标任务模式

多目标任务模式:让任务根据配置形成多个子任务

  1. 修改 gruntfile.js,添加多目标任务 build

    module.exports = (grunt) => {
      grunt.registerMultiTask("build", function () {
        console.log("hello world");
      });
    
      grunt.registerTask("default", ["build"]);
    };
    
  2. 在配置方法中,添加任务同名的对象,其中包含多目标

    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"]);
    };
    
  3. 使用命令 npm exec grunt

  4. 多目标对象中的 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"]);
    };
    
  5. 使用命令 npm exec grunt

e. 插件使用

自动清除临时文件插件为例

  1. 使用命令 npm install grunt-contrib-clean

  2. 修改 gruntfile.js,导入并配置

    module.exports = (grunt) => {
      grunt.initConfig({
        clean: {
          temp: "temp/**",
        },
      });
    
      grunt.loadNpmTasks("grunt-contrib-clean");
    };
    

(3)Gulp

a. 基础使用

  1. 使用命令 npm init -y 快速创建 NodeJS 环境

  2. 使用命令 npm install --save-dev gulp 安装 Gulp

  3. 在项目根目录下创建 gulpfile.js

    // Gulp 入口文件
    
    module.exports = {
      default: (done) => {
        console.log("hello world");
        done();
      },
    };
    

    最新的 Gulp (5.0.0)取消了同步任务模式,而是全部采用异步任务模式,需要通过回调函数等方式标记任务完成

  4. 使用命令 npm exec gulp default 启动 Grunt 构建工具并执行名为 default 的任务
    由于 default 也是默认任务,因此可以使用命令 npm exec gulp 执行 default 任务

b. 组合任务

组合任务包括串行任务并行任务

  1. 修改 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);
    };
    
  2. 使用 series 组合串行任务

    const { series } = require("gulp");
    
    // ...
    
    module.exports = {
      default: series(task1, task2, task3),
    };
    

    执行顺序为 1 2 3

  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 + Promise

    const 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 文件压缩为例

  1. 创建一个样式文件 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;
    }
    
  2. 修改 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 文件压缩为例

    1. 使用命令 npm install --save-dev gulp-clean-css gulp-rename 安装相关插件

    2. 修改 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. 基础使用

  1. 使用命令 npm install -g fis3 全局安装 FIS

  2. 在项目根目录下创建 fis-conf.js,实现资源定位

    // FIS 配置文件
    
    fis.match('*.{js}', {
      release: "/assets/$0"
    });
    
  3. 在已有项目目录下,使用命令 fis3 release -d dist 将 FIS 构建输出到当前目录下的 dist 目录中

b. 插件使用

编译 SASS 文件为例

  1. 使用命令 npm install -g fis-parser-node-sass 全局安装 SASS 编译器

  2. 修改 fis-conf.js

    fis.match("**/*.scss", {
      rExt: ".css",
      parser: fis.plugin("node-sass");
    });
    
  3. 使用命令 fis3 release -d dist 发布

-end-

posted @ 2024-12-11 16:11  SRIGT  阅读(6)  评论(0编辑  收藏  举报