前端工程化

工程化就是将一些重复繁琐的工作让计算机自动处理,以节省人力,我们今天来看看前端工程化是怎么一个过程。

# 之前存在哪些问题

前端经过多年发展,经过刀耕火种到现在,发展过程中也遇到很多问题,技术都是为了解决问题而催生的。

  1. 日常想要使用新语法,但是又不得不考虑兼容性问题
  2. 想要使用less/scss/postcss等css增强,但是运行环境一般都不支持
  3. 想要使用新的模块化,组件化的方式提高维护性,运行环境一般也不支持
  4. 压缩代码,上传部署等重复工作
  5. 编码风格及规范在多人协作时不容易统一
  6. 开发时,很多接口服务需要依赖后端接口完成后才可以进行

以上问题都可以通过工程化解决

# 工程化如何解决问题

  1. 创建项目时,我们可以通过脚手架工具初始化项目
  2. 编码时,我们可以格式化代码,规范风格,编译新特性,兼容性构建以及压缩打包
  3. 开发/测试时,mock数据,开发服务器,热更新,以及使用sourceMap方便调试
  4. 提交时,git hooks 检查,lint规范检查;持续集成
  5. 部署时,自动发布;

# 脚手架工具

脚手架工具主要作用就是创建项目基础结构,提供项目规范和约定。大致包含以下方面

  1. 相同的组织结构
  2. 相同的开发范式
  3. 相同的模块依赖
  4. 相同的工具配置
  5. 相同的基础代码

脚手架工具就是为我们省去这些重复过程,直接初始化这样一个起手势。

# 常用脚手架工具

不同框架会提供自己的脚手架工具,但功能都大同小异,均是根据信息创建对应但项目基础结构

  • react - create-react-app
  • vue - vue-cli
  • angular - angular-cli

# 通用工具

Yeoman;一种是通用的工具,他可以根据模版生成项目对应结构;具体的他是根据不同的生成器(自定义生成器)生成任意的项目结构。

所以yeoman 脚手架使用分为以下步骤

  1. 明确需求
  2. 找到合适的generator,这里已有大量generator,查找 (opens new window)
  3. 全局安装对应生成器(generator) - 如: npm i generator-node
  4. 通过yo命令运行generator -如:yo node
  5. 命令行填写项目信息

填写完成就可以自动创建完成了;

脚手架工具其实就是接收一些预设,通过用户输入,结合模版生成项目结构。

 yo yo node
? Module Name demo-yo
? Description demo-yo
? Project homepage url
? Author's Name jason
? Author's Email demo@mail.com
? Author's Homepage
? Package keywords (comma to split) demo
? Send coverage reports to coveralls No
? Enter Node versions (comma separated)
? GitHub username or organization
? Which license do you want to use? (Use arrow keys)
❯ Apache 2.0
  MIT
  Mozilla Public License 2.0
  BSD 2-Clause (FreeBSD) License
  BSD 3-Clause (NewBSD) License
  Internet Systems Consortium (ISC) License
  GNU AGPL 3.0
(Move up and down to reveal more choices)

我们还可以创建自己的脚手架

# 自动化构建

计算机擅长的事就是根据命令及程序,不知疲倦自动完成指令。这里,我们需要他帮我们自动化把开发代码转换为生成代码。 这让我们在开发阶段尽量不考虑生成环境,可以去使用一些高级的语法,新的规范标准,而不用过多考虑兼容性。

首先是npm script ,这个配置在package.json中的属性,可以配置自定义运行命令,并和项目一起维护。

 "scripts": {
    "dev": "vuepress dev docs",
    "build": "vuepress build docs",
    "deploy": "sh ./shell/deploy-test.sh",
    "service": ""
  }

# Grunt

最早的自动化工具,插件非常完善,官方号称可以完成任何想做的事。 然而Grunt有他的缺陷,他的工作过程是基于临时文件的,每一步工作流会有大量的磁盘读写,这就导致速度上会有劣势;

举个例子,如果我们需要使用他进行scss构建;先编译再添加属性前缀,最后再压缩代码。这三步Grunt每一步都会产生临时文件,磁盘读写 ,scss编译完后,它会将结果保存,然后在读取这个文件进行下一步,步数越多,磁盘读写越多,势必会影响整体构建速度。

// gruntfile.js
// Grunt 的入口文件
// 用于定义一些需要 Grunt 自动执行的任务
// 需要导出一个函数
// 此函数接收一个 grunt 的对象类型的形参
// grunt 对象中提供一些创建任务时会用到的 API

module.exports = grunt => {
    grunt.registerTask('print', () => {
        console.log('task print')
    })
    // 异步任务
    // 由于函数体中需要使用 this,所以这里不能使用箭头函数
    grunt.registerTask('async-task', function () {
        const done = this.async()
        setTimeout(() => {
            console.log('async task working~')
            done()
            // done(false) 异步任务失败在这里返回
        }, 1000)
    })
    // 任务失败,会导致串联的任务直接返回
    grunt.registerTask('fail', () => {
        console.log('task fail')
        return false // 返回false即为失败
    })
    grunt.registerTask('default','描述:这个任务默认导出,直接运行 npm grunt,数组中依次执行',['print','fail'])
}

// 命令行运行 npm grunt print

> npm grunt print
Running "print" task
task print

Done.

# Gulp

相比Grunt ,Gulp解决了这个速度问题,他是基于内存实现每一步的产出的,这大大提高了运行速度。而且,gulp可以同时执行多个任务,这也大大提高了执行效率。 另外使用方式他也会简单许多,插件也很丰富,所以后来居上,Gulp比较流行。

下面展示了gulp常规工作流程,即读取流,转换流,输出流的三步。

// 读取js文件,替换掉空格,输出文件
const fs = require('fs')
const {Transform} = require('stream')

exports.zipJs = () => {
    const read = fs.createReadStream('gulpfile.js')
    const write = fs.createWriteStream('gulpfile.txt')
    const transform = new Transform({
        transform(chunk, encoding, cb) {
            const input = chunk.toString()
            const output = input.replace(/\s+/g, '')
            cb(null, output) // 第一个参数为错误,没有则传null
        }
    })
    read.pipe(transform).pipe(write)
    return read
}

此外,gulp封装了更易于使用的流api

const {src,dest} = require('gulp')
const  clean =require('gulp-clean-css')

exports.test = ()=>{
    return src('style.css')
        .pipe(clean()) // 压缩css
        .pipe(dest('dist')) // 移动文件到此目录
}

同样的,gulp也有大量的插件 (opens new window) 下面是一个综合例子

const { src, dest, parallel, series, watch } = require('gulp')

const del = require('del')
const browserSync = require('browser-sync')
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins() // 自动加载使用的插件
const bs = browserSync.create()

const data = {
  menus: [
    {
      name: 'Home',
      icon: 'aperture',
      link: 'index.html'
    },
    {
      name: 'Features',
      link: 'features.html'
    },
    {
      name: 'About',
      link: 'about.html'
    },
    {
      name: 'Contact',
      link: '#',
      children: [
        {
          name: 'Twitter',
          link: 'https://twitter.com/w_zce'
        },
        {
          name: 'About',
          link: 'https://weibo.com/zceme'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/zce'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}

const clean = () => {
  return del(['dist', 'temp'])
}
// 处理scss文件
const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })
    .pipe(plugins.sass({ outputStyle: 'expanded' }))
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}
// 转译js
const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}
// html文件处理
const page = () => {
  return src('src/*.html', { base: 'src' })
    .pipe(plugins.swig({ data, defaults: { cache: false } })) // 防止模板缓存导致页面不能及时更新
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}
// 压缩图片
const image = () => {
  return src('src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin()) // 仅去除图片附加信息,不改变图片质量
    .pipe(dest('dist'))
}
// 字体文件处理
const font = () => {
  return src('src/assets/fonts/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}
// 移动文件
const extra = () => {
  return src('public/**', { base: 'public' })
    .pipe(dest('dist'))
}
// 监听文件变化
const serve = () => {
  watch('src/assets/styles/*.scss', style)
  watch('src/assets/scripts/*.js', script)
  watch('src/*.html', page)
  // watch('src/assets/images/**', image)
  // watch('src/assets/fonts/**', font)
  // watch('public/**', extra)
  watch([
    'src/assets/images/**',
    'src/assets/fonts/**',
    'public/**'
  ], bs.reload)
  // 初始化服务器
  bs.init({
    notify: false,
    port: 2080,
    // open: false,
    // files: 'dist/**',
    server: {
      baseDir: ['temp', 'src', 'public'],
      routes: {
        '/node_modules': 'node_modules' // 引入文件映射
      }
    }
  })
}

const useref = () => {
  return src('temp/*.html', { base: 'temp' })
      // 解析 HTML 文件中的构建块以替换对未优化脚本或样式表的引用。
    .pipe(plugins.useref({ searchPath: ['temp', '.'] }))
    // html js css 压缩
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
      collapseWhitespace: true,
      minifyCSS: true,
      minifyJS: true
    })))
    .pipe(dest('dist'))
}

const compile = parallel(style, script, page)

// 上线之前执行的任务
const build =  series( // 串行执行
  clean,
  parallel( // 并行执行
    series(compile, useref),
    image,
    font,
    extra
  )
)
// 封装任务
const develop = series(compile, serve)

module.exports = {
  clean,
  build,
  develop
}

# 模块化打包

这方面就是webpack之类的打包工具的主场了,可以看之前文章

# 项目规范化

规范化也是前端工程化的一大部分; 为什么需要规范化,软件开发是多人协同的,不同的人有不一样的习惯,不规范会导致维护成本上升。

# 哪些需要规范

  1. 代码 - js,html,css
  2. 文档
  3. 提交日志 - git提交日志按类型区分

# 使用的工具

各种lint工具可以很好的很好的规范化我们遇到的问题,也可以很方便的自动化处理。

  • ESLint - 最主流的JS/TS Lint工具
  • StyleLint - css/less/sass/PostCss的Lint工具
  • Prettier - 代码格式化工具
  • Git Hooks - 提交时强制检查

// Husky 实现 git hooks

// package.json 文件中增加
{
    "husky":{
        "hooks":{
            "pre-commit":"npm run lint" // 提交前运行此命令
        }
    }
}

这个时候 commit 就会先自动执行 npm run lint 了,然后才会 commit。

但是这样解决了以上的问题,当项目大的时候会遇到一些问题,比如每次 lint 是整个项目的文件,文件太多导致跑的时间过久,另外如果这个 lint 是在项目后期接入的话,可能 lint 命令会报很多错误,全量去改可能会有问题。

lint-staged

基于上面的痛点,lint-staged 就出现了,它的解决方案就是只检查本次提交所修改(指 git 暂存区里的东西)的问题,这样每次 lint 量就比较小,而且是符合我们的需求的。

// package.json 文件中增加
{
  "lint-staged":{
    "**/*.less": "stylelint --syntax less",
    "**/*.{js,jsx,ts,tsx}": "eslint --ext .js,.jsx,.ts,.tsx",
    "**/*.{js,jsx,tsx,ts,less,md,json}": [
      "prettier --write",
      "git add"
    ]
  }
}

这些工具对于检查代码质量,规范风格,修复部分错误都不在话下。使用方式也大同小异,可以通过配置文件管理规则,再集成到构建工具中自动化就可以了。

# 部署自动化

一般主流开源使用的是jenkins,它可以根据git钩子触发自动构建发布;

最近更新
01
echarts扇形模拟镜头焦距与可视化角度示意图
03-10
02
vite插件钩子
03-02
03
vite的依赖预构建
02-13
更多文章>