前端工程化-实践总结

/post/fe-engineering article cover image

前端工程化是指遵循一定标准和规范通过工具从开发、生产到部署的解决方案,用以解决日益复杂的前端业务需求来提升效率和优化开发体验。

项目初始化

除了框架提供的官方脚手架外(rect、vue、angular、svelte,etc),还有几种用于自定义创建项目模版的工具

yeoman

常规使用:搭配generator使用,执行对应命令后通过交互生成

shell
pnpm add -g yo

pnpm add -g generator-travis

cd your-work-directory

yo travis

创建自定义生成器:

  • 特定的项目结构
md
├───package.json     // 配置文件
└───generators/
    ├───app/
    │   └───index.js // 入口文件
    └───router/
        └───index.js // 子命令文件
  • 扩展生成器
js
var Generator = require('yeoman-generator');
// 返回继承yeoman生成器的子类用于扩展
module.exports = class extends Generator {
  constructor(args, opts) {
    super(args, opts)
    this.option('xxx'); // 增加--xxx命令表示
  }
  // 自定义方法及逻辑
  writing() {}   // 文件写入、拷贝
  promoting() {} // 用户命令行交互
};
  • 链接本地到全局并运行
shell
pnpm link

yo your-packagejson-name<generator-demo>

yeoman提供了很多自定义化api以供使用包括 文件读写拷贝、命令行提示、自定义指令等 ,且不局限任何项目只需要关注要创建项目的具体自定义逻辑即可

plop

编写一个创建page指令的脚本

  • 项目结构
md
├───package.json     // 配置文件
└───templates/       // hbs模版
    ├───page.hbs     // page模版
└───src/
    ├───pages/
    ├───xxx          // 要被新增的page
└───plop.js          // plop脚本文件
└───xxx

  • 常规使用:全局安装并在当前项目下创建plop.js编写文件创建
shell
pnpm add -g plop

cd your-workspace

mkdir plop.js
  • 编写逻辑:
js
module.exports = prop => {
  plop.setGenerator('page', {
    description: 'create a page', // 任务描述
    prompts: [                    // 命令行用户交互
      {
        type: 'input',
        name: 'name',
        message: 'page\'s name',
        default: 'myPage'
      }
    ],
    actions: [                    // 对应输入的执行指令
      {
        type: 'add',
        path: 'src/pages/{[name]}/{[name]}.js',
        templateFile: 'templates/page.hbs'
      }
    ]
  })
}
  • 执行命令:
shell
pnpm plop page

Plop更加专注特定模版文件的创建且侵入性较小不会对原有项目结构进行破坏且扩展成本较小

nx-generator

Nx作为现代monorepo开发模式的工作流同样也覆盖了自定义模版的创建以及各种构建阶段的任务执行器

  • 初始化项目:
shell
// 安装所需依赖
pnpm add -D nx @nx/plugin@latest @nx/devkit

// 使用`nx generator`必须是基于nx工作流基础项目,所以需要先创建`nx workspace`然后创建`generator`目录
pnpm create-nx-workspace@latest nx-project

// excutor和generator都属于plugin的范畴,需要按照顺序创建目录
pnpm nx g @nx/plugin:plugin my-plugin

// 在plugin包项目中创建generator
pnpm nx generate @nx/plugin:generator my-generator --project=my-plugin

初始化完成后的目录结构:

md
nx-project/
├── apps/
├── libs/
│   ├── my-plugin
│   │   ├── src
│   │   │   ├── generators
│   │   │   |   └── my-generator/
│   │   │   |   |    ├── files/
│   │   │   |   |    ├── files/src/index.ts.template
│   │   │   |   |    ├── generator.spec.ts
│   │   │   |   |    ├── generator.ts
│   │   │   |   |    ├── schema.d.ts
│   │   │   |   |    └── schema.json
├── nx.json
├── package.json
└── tsconfig.base.json
  • 编写逻辑:

生成器配置文件包含了用户命令行交互等

json
{
  "$schema": "http://json-schema.org/schema",
  "$id": "MyGenerator",
  "title": "",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "",
      "$default": {
        "$source": "argv",
        "index": 0
      },
      "x-prompt": "What name would you like to use?"
    }
  },
  "required": ["name"]
}

逻辑入口文件

ts
import {
  formatFiles,
  generateFiles,
  Tree,
} from '@nx/devkit';
import * as path from 'path';
import { MyGeneratorGeneratorSchema } from './schema';

export async function myGeneratorGenerator(
  tree: Tree,
  options: MyGeneratorGeneratorSchema
) {
  const projectRoot = `libs/${options.name}`;
  generateFiles(tree, path.join(__dirname, 'files'), projectRoot, options);
  await formatFiles(tree);
}

export default myGeneratorGenerator;
  • 运行命令
shell
pnpm nx g my-plugin:my-generator

@nx/devkit包含了编写generator所需的文件读写、拷贝、ejs模版语法替换等工具函数以供方便使用,具体请参考 local generator @nx/devkit

总结

  • Yoeman更适合需要高度自定义(与前端业务、框架无关)模版的场景,但编写相对繁琐且范式需要单独维护
  • Plop更专注于模版文件的操作功能较为单一,与业务耦合较少
  • Nx-Generator更适合现代monorepo开发模式几乎覆盖到所有开发流程所需功能,但必须是基于nx的项目与业务联系紧密

自动化构建

通过流程化工具将单个或多个功能任务进行自动化执行的过程就是自动化构建,常见的自动化构建工具有:

grunt

插件化工作流处理几乎能完成所需场景,但是由于它的工作过程是基于临时文件的每个流程节点必须对磁盘进行读写所以效率较低

js
module.exports = grunt => {
  // 初始化配置
  grunt.initConfig({
    env: {
      ip: 'xxx'
    }
  })

  // 同步任务
  grunt.registerTask('first', () => {
    console.log('first', grunt.config.env)
  })
  grunt.registerTask('last', 'task description', () => {
    console.log('last task')
  })
  // 标记错误
  grunt.registerTask('fail', () => {
    console.log('will bot be consoled')
    return false;
  })

  // 默认任务
  grunt.registerTask('default', ['first', 'last'])

  // 异步任务
  grunt.registerTask('asyncTask', function() {
    const done = this.await();
    setTimeout(() => {
      console.log('async task running')
      done()
    }, 1000)
  })

  // 多目标任务
  grunt.initCOnfig({
    build: {
      js: '1',
      css: '1',
      options: {
        // 内建选项,只能通过this.options()获取
      }
    }
  })
  grunt.registerMultiTask('build', function() {
    console.log(`target: ${this.target}, data: ${this.data}`)
  })

  // 使用插件
  grunt.initConfig({
    clean: {
      // temp: 'temp/app.js'
      // temp: 'temp/*.js'
      temp: 'temp/**'
    }
  })
  grunt.loadNpmTasks('grunt-contrib-clean')
}

gulp

拥有与Grunt相似的插件化及任务流处理,但是IO读写是在内存中处理所以速度相对较快,与webpack不同在一般用于对源码的转换而非模块化的处理

可通过查看此 gulp-demo-starter ,查看具体实际使用用例

fis3

由百度团队从PHP版本迁移过来的构建工具版本,覆盖整个项目的开发工作流资源处理、模块化开发、依赖分析、静态资源加载等一系列大而全的解决方案

shell
pnpm add -g fis3

cd your-workspace

mkdir fis-config.js
  • 构建逻辑编写:
js
fis.match('*.{js,scss,png}', {
	release: '/assets/$0' // 当前文件原始目录
})

fis.match('**/*.scss', {
	rExt: '.css',
  parser: fis.plugin('node-sass'),
  optimizer: fis.plugin('clean-css')
})

fis.match('**/*.js', {
  parser: fis.plugin('babel-6.x'),
  optimizer: fis.plugin('uglify-js')
})
  • 运行构建:
shell
fis realease

总结-1

  • Grunt: 各构建流程读写都依赖本地磁盘效率较低且已经不在维护
  • Gulp: 与Grunt类似的任务处理流程但是操作读写在内存中速度更快,其工作流以及自定义化的任务调度对于特定场景至今仍受欢迎(elementui2.x就是用到gulp用作transform)
  • FIS3: 不同于上面两个是一个与业务联系密切功能高度集成的解决方案且已经不在维护,但其实践价值值得参考

项目构建

这里只列出常见模块打包工具的优缺点,具体使用场景不做阐述:

webpack

  • 优点: 生态丰富、社区活跃几乎满足所有的前端模块化应用开发,在满足开发体验(HMR热更新)和生产优化(splitChunk分包策略等)的同时提供了插件式(tapable)架构满足开发者自定义需求
  • 缺点: 由于高度自定义带来的配置学习成本以及由于架构本身原因导致的随着项目增大构建速度慢的问题

rollup

  • 优点: 专注于esmodule模块化类库的打包器,相对webpack更加轻量、更好的构建产物性能(tree-shaking、产物扁平化等)、更加易懂的插件开发hooks
  • 缺点: 由于其本身更倾向于类库打包,对于应用类开发生态和模块分包方面只有manualChunks输出选项配置扩展性和灵活性不如webpack,不过由于后期vite的出现弥补了这方面的短缺

parcel

  • 优点: 主打零配置、内置支持各种文件类型减少使用者心智负担且2.0之后使用rust重写性能较于前面两者有明显的提升
  • 缺点: 对于复杂的自定义需求解决和学习曲线较高

vite

  • 优点: 基于原生esmodule的模块解析及bundless模式的增量构建使得拥有极速的冷启动时间和热加载,兼容并扩展自rollup的插件系统使得应对自定义场景更加灵活,社区生态也相对活跃
  • 缺点: 仅适用支持原生esmodule的浏览器,尽管有vite-plugin-legacysystemjs降级处理但是在部分场景还是不如webpack成熟,且生产构建速度因为使用的rollup所以并没有开发速度那么快

turbopack

webpack作者亲自操刀使用rust重写的构建工具,仅仅是Beta版本文档也不那么完成目前只在nextjs团队内部使用,暂且搁置

rspack

字节跳动使用rust重写的webpack版本,不单单是重写同时借鉴viteturbopack的优点进行优化,目前处于Beta版本,暂且搁置

代码规范

在团队中代码风格、规范必不可少,同样的可以使用业内较为流行的工程化解决方案集成到项目当中

eslint

在业内对代码进行规范并约束的手段被称为lint,ESLint就是对javascript项目进行语法分析找出问题并提示修复的静态分析工具,旨在统一代码风格、减少错误、提升代码健壮性的作用

shell
pnpm init

pnpm craete @eslint/config

pnpm eslint xxx.js

pnpm esint lin/**

// 查看所有命令
pnpm eslint -h

ESLint的主要配置项为:EnviromentsGlobalsRulesExtendsPlugins,这些选项都在.eslintrc.*文件或package.json > eslintConfig中配置:

  • 使用配置注释:
js
/* global var1, var2 */
/* eslint-disable-next-line */
  • 使用配置文件
js
module.exports = {
  root: true, // 指定根目录属性,不会向父级目录查找
  env: {      // 设置环境变量
    node: true,
    browser: true
  },
  extends: [  // 使用共享配置
    "eslint:recommend"
  ],
  parser: "@typescript-eslint/parser", // 指定语法解析器
  plugins: [  // 使用插件预设
    "eslint-plugin-xxx"
  ],
  processor: "x-plugin/processor", // 使用指定处理器
  rules: {    // 设置指定规则项
    eqeqeq: 'off',
    curly: 'error',
    semi: ['warn', 'always']
  },
  override: [ // 覆盖原有配置项
    {
      files: ['*-xxx.js'],
      rules: {
        "no-unused-expressions": "off"
      }
    }
  ],
  ignorePatterns: [ // 忽略模式
    'xxx.js',
    '/some/**/*.js'
  ]
}
  • 构建工具集成(eslint-loader):
js
modules.exports = {
   module: {
    rules: [
      // ... other rules ...
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ['eslint-loader']
      }
    ]
  }
}

stylelint

eslint较为类似,功能没有前者丰富具体配置和集成方式可以参考前者

prettier

代码格式化工具,用于团队统一代码风格

  • 命令行:
shell
pnpm add -D prettier

touch .prettierrc

pnpm prettier xxx --write
  • 配置文件:
text
{
  "endOfLine": "lf",
  "semi": false,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "es5"
}

rome

rust编写的致力于打包前端统一(代码格式化、代码检查、编译、打包、测试)工具链,目前处于Beta版本暂且搁置

代码提交

一般就代码提交前进行一些格式化及规则校验

git-hook

此方案相对繁琐且需要shell学习成本

shell
#!/bin/sh

# Run linting before committing
echo "Running linting..."
npm run lint

# If linting fails, prevent the commit
if [ $? -ne 0 ]; then
  echo "Linting failed. Please fix the errors before committing."
  exit 1
fi

eslint结合husky

通过插件自定义配置的方式对commit hook增加对应处理

shell
// 安装依赖, ...省略eslint配置
pnpm add -D eslint husky lint-staged

使用husky监听git hook并执行对应脚本命令并通过lint-staged对对应钩子阶段进行细粒度控制

json
{
  "scripts": {
    "lint": "lint-staged"
  },
  "husky": {
    "hooks": {
      "pre-commit": "npm run lint"
    }
  },
  "lint-staged": {
    "*.js": [
      "eslint",
      "git add"
    ]
  }
}