前端工程化-实践总结
前端工程化是指遵循一定标准和规范通过工具从开发、生产到部署的解决方案,用以解决日益复杂的前端业务需求来提升效率和优化开发体验。
项目初始化
除了框架提供的官方脚手架外(rect、vue、angular、svelte,etc),还有几种用于自定义创建项目模版的工具
Yeoman
常规使用:搭配generator
使用,执行对应命令后通过交互生成
pnpm add -g yo
pnpm add -g generator-travis
cd your-work-directory
yo travis
创建自定义生成器:
- 特定的项目结构
├───package.json // 配置文件└───generators/ ├───app/ │ └───index.js // 入口文件 └───router/ └───index.js // 子命令文件
- 扩展生成器
var Generator = require('yeoman-generator');// 返回继承yeoman生成器的子类用于扩展module.exports = class extends Generator { constructor(args, opts) { super(args, opts) this.option('xxx'); // 增加--xxx命令表示 } // 自定义方法及逻辑 writing() {} // 文件写入、拷贝 promoting() {} // 用户命令行交互};
- 链接本地到全局并运行
pnpm link
yo your-packagejson-name<generator-demo>
yeoman提供了很多自定义化api以供使用包括文件读写拷贝、命令行提示、自定义指令等,且不局限任何项目只需要关注要创建项目的具体自定义逻辑即可
Plop
编写一个创建page
指令的脚本
- 项目结构
├───package.json // 配置文件└───templates/ // hbs模版 ├───page.hbs // page模版└───src/ ├───pages/ ├───xxx // 要被新增的page└───plop.js // plop脚本文件└───xxx
- 常规使用:全局安装并在当前项目下创建
plop.js
编写文件创建
pnpm add -g plop
cd your-workspace
mkdir plop.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' } ] })}
- 执行命令:
pnpm plop page
Plop
更加专注特定模版文件的创建且侵入性较小不会对原有项目结构进行破坏且扩展成本较小
Nx generator
Nx
作为现代monorepo
开发模式的工作流同样也覆盖了自定义模版的创建以及各种构建阶段的任务执行器
- 初始化项目:
// 安装所需依赖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包项目中创建generatorpnpm nx generate @nx/plugin:generator my-generator --project=my-plugin
初始化完成后的目录结构:
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
- 编写逻辑:
生成器配置文件包含了用户命令行交互等
{ "$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"]}
逻辑入口文件
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;
- 运行命令
pnpm nx g my-plugin:my-generator
@nx/devkit
包含了编写generator
所需的文件读写、拷贝、ejs模版语法替换等工具函数以供方便使用,具体请参考local generator和@nx/devkit
总结
Yoeman
更适合需要高度自定义(与前端业务、框架无关)模版的场景,但编写相对繁琐且范式需要单独维护Plop
更专注于模版文件的操作功能较为单一,与业务耦合较少Nx-Generator
更适合现代monorepo
开发模式几乎覆盖到所有开发流程所需功能,但必须是基于nx
的项目与业务联系紧密
自动化构建
通过流程化工具将单个或多个功能任务进行自动化执行的过程就是自动化构建,常见的自动化构建工具有:
Grunt
插件化工作流处理几乎能完成所需场景,但是由于它的工作过程是基于临时文件的每个流程节点必须对磁盘进行读写所以效率较低
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版本迁移过来的构建工具版本,覆盖整个项目的开发工作流资源处理、模块化开发、依赖分析、静态资源加载等一系列大而全的解决方案
pnpm add -g fis3
cd your-workspace
mkdir fis-config.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')})
- 运行构建:
fis realease
总结
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-legacy
的systemjs
降级处理但是在部分场景还是不如webpack
成熟,且生产构建速度因为使用的rollup
所以并没有开发速度那么快
turbopack
由webpack
作者亲自操刀使用rust
重写的构建工具,仅仅是Beta版本文档也不那么完成目前只在nextjs
团队内部使用,暂且搁置
rspack
字节跳动使用rust
重写的webpack版本,不单单是重写同时借鉴vite
、turbopack
的优点进行优化,目前处于Beta版本,暂且搁置
代码规范
在团队中代码风格、规范必不可少,同样的可以使用业内较为流行的工程化解决方案集成到项目当中
ESLint
在业内对代码进行规范并约束的手段被称为lint
,ESLint就是对javascript项目进行语法分析找出问题并提示修复的静态分析工具,旨在统一代码风格、减少错误、提升代码健壮性的作用
pnpm init
pnpm craete @eslint/config
pnpm eslint xxx.js
pnpm esint lin/**
// 查看所有命令pnpm eslint -h
ESLint
的主要配置项为:Enviroments
、Globals
、Rules
、 Extends
、Plugins
,这些选项都在.eslintrc.*
文件或package.json > eslintConfig
中配置:
- 使用配置注释:
/* global var1, var2 *//* eslint-disable-next-line */
- 使用配置文件
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):
modules.exports = { module: { rules: [ // ... other rules ... { test: /\.(js|jsx)$/, exclude: /node_modules/, use: ['eslint-loader'] } ] }}
StyleLint
与eslint
较为类似,功能没有前者丰富具体配置和集成方式可以参考前者
Prettier
代码格式化工具,用于团队统一代码风格
- 命令行:
pnpm add -D prettier
touch .prettierrc
pnpm prettier xxx --write
- 配置文件:
{ "endOfLine": "lf", "semi": false, "singleQuote": false, "tabWidth": 2, "trailingComma": "es5"}
Rome
由rust
编写的致力于打包前端统一(代码格式化、代码检查、编译、打包、测试)工具链,目前处于Beta版本暂且搁置
代码提交
一般就代码提交前进行一些格式化及规则校验
git hook
此方案相对繁琐且需要shell
学习成本
#!/bin/sh
# Run linting before committingecho "Running linting..."npm run lint
# If linting fails, prevent the commitif [ $? -ne 0 ]; then echo "Linting failed. Please fix the errors before committing." exit 1fi
ESLint结合Husky
通过插件自定义配置的方式对commit hook
增加对应处理
// 安装依赖, ...省略eslint配置pnpm add -D eslint husky lint-staged
使用husky
监听git hook
并执行对应脚本命令并通过lint-staged
对对应钩子阶段进行细粒度控制
{ "scripts": { "lint": "lint-staged" }, "husky": { "hooks": { "pre-commit": "npm run lint" } }, "lint-staged": { "*.js": [ "eslint", "git add" ] }}