大前端进阶-工程化

概述

前端工程化是使用软件工程的技术和方法来进行前端的开发流程、技术、工具、经验等规范化,标准化,主要目的是为了提升效率和下降成本。

目前,前端项目愈来愈复杂化和多元化,可是随着变化而来的就是以下问题:javascript

  • 传统语言或语法有弊病。虽然ES6及后续版本提出不少解决方案,可是因为环境的支持程度不一样,须要进行大量重复性的适配工做。
  • 没法模块化,组件化。简单来讲,模块化指的是将一个文件拆分红多个相互独立的小文件,使用的时候再按照必定的规则加载和拼接。组件化是指将UI拆分红一个个功能独立单一的结构单元。
  • 存在大量的重复的机械性工做。如项目的构建,发布等。
  • 代码风格不统一,没法保证质量。
  • 严重依赖后端接口支持。
  • 其余问题。

前端工程化为上述问题提供了成熟的解决方案,做为使用者,能够更加关注业务逻辑的实现,也就提升了效率,下降了成本。php

脚手架工具

建立项目的第一步就是利用脚手架工具建立项目模版,目前流行的框架均提供了脚手架工具,如react的create-react-app,vue的vue-cli。这些脚手架工具不只构建了统一的项目结构,同时提供了语法转换、模块化组件化、代码风格检查、单元测试、自动构建部署等方案。css

Yeoman是一种开源的脚手架工具,其相比vue-cli专门用于vue项目不一样,其更加灵活,能够基于不一样的generator生成不一样的项目。所以本篇文章将从Yeamon入手,探索如何搭建脚手架。html

使用Yeoman建立项目

  • 安装yo: npm install -g yo。
  • 根据想要建立的项目类型使用相应generator,咱们建立一个webapp,所以使用generator-webapp:npm install -g generator-webapp(Yeoman提供了generator查找命令,可一键查找安装)。
  • 在项目根文件夹下执行:yo webapp。

建立自定义Generator

使用Yeoman建立自定义脚手架,就是建立generator。前端

  • 安装generator-generator: npm install generator-generator。

generator-generator是可用于生成generator模版,运行yo generator可建立模版项目,项目结构以下:vue

.
├── generators/
│   └── app/
│       ├── index.js
│       └── templates/
│           └── dummyfile.txt
├── .editorconfig
├── .eslintignore
├── .gitattributes
├── .gitignore
├── .travis.yml
├── .yo-rc.json
├── LICENSE
├── README.md
├── package.json
└── __tests__/
    └── app.js

主要逻辑在index.js中,templates文件夹包含全部模版文件。java

  • index.js文件导出一个继承自Generator的类,其包含了一些配置,控制台交互,文件操做等方法。
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
  // 执行控制台与用户的交互
  prompting() {
    const prompts = [
      {
        type: 'input',
        name: 'name',
        message: 'What is your Project Name?',
        default: 'cus-project'
      }
    ];

    return this.prompt(prompts).then(props => {
      // 保存用户的输入或者选择
      this.props = props;
    });
  }

  // 执行文件操做
  writing() {
    this.fs.copyTpl(
      // 源文件
      this.templatePath('dummyfile.txt'),
      // 目标文件
      this.destinationPath('test.txt'),
      this.props
    )
  }
  // 自动安装依赖
  install() {
    this.npmInstall();
  }
};

能够在模版文件中用<%=name %>使用用户props。node

  • 将编写的包上传到npm,此处因为咱们是测试,可使用:npm link。
  • 在须要建立项目的文件夹下执行yo customeGeneratorName(如咱们的项目名为generator-test,此处就应该是test)。

实现自定义脚手架工具

从自定义generator示例中能够看出,实现一个简单的脚手架主要是实现如下两个方面的内容:react

  1. 实现与用户之间的交互(控制台交互)。
  2. 实现文件的模版替换和复制操做。

在实现自定义脚手架工具以前,得须要了解下面内容:git

  • 如何在npm包中添加可执行文件?

在npm包的pakage.json文件中添加bin属性,bin的值是一个对象:

{
    // 键表示命令,值表示在终端输入命令后执行的文件
    "create-custom": "index.js"
}

当项目中install这个包的时候,命令会注册到全局或者./node_modules/.bin/目录里。

在执行文件的开头须要加上`#!/usr/bin/env node`,不然不会被识别。
  • inquirer

inquirer用于快速建立交互式命令行。其基本使用以下:

#!/usr/bin/env node
const inquirer = require('inquirer')
// 设置问题
inquirer.prompt([
    {
        type: 'input', // 问题类型
        name: 'name', // 数据属性名
        message: '名称', // 提示信息
        default: 'Rogan' // 默认值
    },
    {
        type: 'list',
        name: 'data',
        message: '选择语言',
        choices: [
            {
                name: 'javascript', value: 1
            },
            {
                name: 'go', value: 2
            }
        ]
    }
]).then(answers => {
    // 处理结果
    console.log(answers)
})

问题选项中的类型包含以下:

  1. input: 输入文本
  2. number: 输入数字
  3. confirm: 是否选择 (y/n)
  4. list: 选择列表
  5. rawlist: 带编号的选择列表
  6. expand: 带缩写选择列表
  7. checkbox: 多选
  8. password: 密码
  9. editor:文本编辑器
  • ejs模版语法

ejs是一种高效的嵌入式JavaScript模板引擎。

let ejs = require('ejs')
ejs.render(`
       选择的语言有<%= languages.join(',')%>
    `, {
    languages: ['php', 'javascript']
})
  • fs

fs是node内置的模块,用于文件的操做。

经过上面几个工具就能够实现简单的脚手架工具,实现目标:获取用户的选择,根据选择编译模版并生成项目。

在index.js文件中:

#!/usr/bin/env node
const inquirer = require('inquirer')
const fs = require('fs')
const ejs = require('ejs')
const path = require('path')

const choices = [
    { name: 'javascript', value: 1 },
    { name: 'php', value: 2 },
    { name: 'go', value: 3 }
]
// 实现命令行交互
inquirer.prompt([
    {
        type: 'checkbox',
        name: 'lang',
        message: '选择语言',
        choices
    }
]).then(answers => {
    // 获取交互内容
    const choiced = answers.lang.map(item => {
        let lan = choices.find(l => l.value === item)
        return lan.name
    })
    // 获取模版文件夹所在路径
    const templatesDir = path.join(__dirname, 'templates')
    // 获取当前命令行执行文件夹路径
    const destDir = process.cwd()
    // 读取模版文件夹下的全部文件
    fs.readdir(templatesDir, function (err, files) {
        if (err) {
            throw err
        }

        files.forEach(file => {
            // 编译模版文件
            ejs.renderFile(path.join(templatesDir, file), { lang: choiced }, (err, result) => {
                if (err) throw err
                // 将编译后的内容拷贝到当前命令行执行文件夹下
                fs.writeFileSync(path.join(destDir, file), result)
            })
        })
    })
})

在模版index.html中:

<html>
    <header></header>
    <body>
        <div>
            用户选择: <%= lang.join(',')%>
        </div>
    </body>
</html>

使用plop

和Yeoman不一样,plop是一个在项目内使用的,能够快速建立指定格式文件的脚手架工具,如在vue编程过程当中,每次建立.vue文件,均须要在文件中手动输入template,script,style三个节点,能够利用此工具一键生成文件,减小大量的重复工做。
使用步骤以下:

  • 安装依赖包 npm install --save-dev plop
  • 在根目录下建立plopfile.js文件,该文件可用于注册命令。
module.exports = function (plop) {
    // 设置生成器
    plop.setGenerator("create-vue-file", {
        description: "建立vue模版文件",
        // 命令行交互
        prompts: [
            {
                type: 'input',  // 交互类型
                name: 'name',   // 参数名称
                message: '请输入vue文件名称', // 交互提示
                default: 'VueFile'
            },
            {
                type: 'input',
                name: 'path',
                message: '请输入文件建立目录'
            }
        ],
        // 交互完成后执行的动做
        actions: [
            {
                type: 'add', // 动做类型: 表示添加文件
                path: '{{ path }}/{{ name }}.vue', // 根据用户输入获取文件路径
                templateFile: 'templates/vue.hbs' // 模板文件地址, 使用hbs文件
            }
        ] // 执行操做
    })

}
  • 添加模版文件,在templates文件加下添加vue.hbs模版文件
<template>
    <div class="{{name}}-container">

    </div>
</template>
<script>
    export default {
        name: {{ name }}
    }
</script>
<style>
    .{{name}}-container {
        
    }
</style>
  • 添加npm scripts

在package.json文件的scripts属性下添加:"plop": "plop"

  • 执行命令

在命令行执行: npm run plop create-vue-file。
根据提示输入文件名和文件路径,最终会生成文件以下:
企业微信20200729043033.png

自动构建

gulp

要想使用gulp,须要下面两个步骤:

  1. 安装gulp包:npm install gulp --save-dev。
  2. 在项目下建立gulpfile.js文件,该文件中注册相应任务等。

基本使用

在gulpfile中添加任务

// gulp要求全部任务均为异步任务,须要经过调用done函数标识任务完成
exports.foo = done => {
    console.log("foo task")
    done()
}

const gulp = require('gulp')
// 在老的版本中,须要经过task方法定义任务
gulp.task("bar", done => {
    console.log("bar task")
    done()
})

在命令行中执行npm run gulp foo,便可执行对应的任务。

组合任务

gulp提供了两个用于组合任务的函数:seriesparallel,前者表示串行任务,即多个任务会依次执行。后者表示并行任务,即多个任务会同时执行。

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

// 建立两个异步任务,因为这两个异步任务并无被导出,所以不能被gulp调用
const task1 = done => {
    setTimeout(() => {
        console.log("task1 ...")
        done()
    }, 1000)
}
const task2 = done => {
    setTimeout(() => {
        console.log("task2 ...")
        done()
    }, 1000)
}

// 定义串行任务,只有task1执行完成后(done方法被调用,意味着执行完成),task2才会执行
exports.foo = series(task1, task2)
// 定义并行任务,可同时执行
exports.bar = parallel(task1, task2)

异步任务

gulp提供了多种定义异步任务的方式:

// 最多见的利用回调函数
exports.callbak = done => {
    setTimeout(() => {
        console.log("callback ...")
        done()
        // done(new Error())
    }, 1000)
}
// 一样支持返回一个promise对象
exports.promise = () => {
    return new Promise((resolve, reject) => {
        console.log('promise ...')
        resolve() // 标识成功
        // reject(new Error()) //标识失败
    })
}

// 支持async await
exports.async = async () => {
    await new Promise((resolve, reject) => {
        console.log('async ...')
        resolve() // 标识成功
        // reject(new Error()) //标识失败
    })
}

const fs = require('fs')
// 因为构建操做大部分针对的是文件操做,所以也执行返回文件流
exports.stream = () => {
    let readStram = fs.createReadStream('package.json')
    let writeStream = fs.createWriteStream('text.txt')
    readStram.pipe(writeStream)
    // 至关于在steam的end事件中执行done方法
    return readStram
}

文件流处理过程

gulp是一种基于流的构建系统,所以最根本的是文件流的处理。最基础的文件流处理过程能够分为三个步骤:输入,处理,输出。

const fs = require('fs')
const { Transform } = require('stream')
exports.file = () => {
    // 获取输入流
    let read = fs.createReadStream('package.json')
    // 获取输出流
    let write = fs.createWriteStream('text.txt')
    const transform = new Transform({
        transform: (chunk, encoding, callback) => {
            const input = chunk.toString()
            // 针对输入的文件内容进行转换操做
            const output = input.replace(/\s+/g).replace(/\/\*.+?\*\//g)
            callback(null, output)
        }
    })
    // 经过管道的方式定义整个文件操做
    read
        .pipe(transform) // 处理输入
        .pipe(write) // 输出
    return read
}

gulp针对文件流读写操做提供了src和dest两个更为强大的方法获取文件读写流,而文件转换操做通常是基于插件完成,经过安装不一样的插件,实现不一样的文件转换。

const { src, dest } = require('gulp')
const cleanCss = require('gulp-clean-css')
const rename = require('gulp-rename')
exports.default = () => {
    return src('src/*.css') //获取src文件夹下的全部css文件
        .pipe(cleanCss()) // 处理一:将css文件压缩
        .pipe(rename({ extname: '.min.css' })) // 处理二: 替换文件的后缀名
        .pipe(dest('dist')) // 将处理后的文件输入到指定的文件夹
}

经常使用实例

本实例目标是经过gulp实现css,js,html编译构建,图片和字体压缩转换。

样式编译

将指定目录下的scss文件转换为css文件。

const { src, dest } = require('gulp')
const sass = require('gulp-sass')
const style = () => {
    return src('src/assets/styles/*.scss', { base: 'src' }) // base属性表示保存源文件从src文件夹开始的文件结构
        .pipe(sass({
            // 指定转换后css显示规则:outputStyle表示结束}放在新的一行
            outputStyle: "expanded"
        }))
        .pipe(dest('dist'))
}

module.exports = {
    style
}
脚本编译

将js中使用的es新特性转换为旧的语法。

const { src, dest } = require('gulp')
const babel = require('gulp-babel')
const script = () => {
    return src('src/assets/scripts/*.scss', { base: 'src' }) // base属性表示保存源文件从src文件夹开始的文件结构
        .pipe(babel({
            // 指定具体的转换工具
            presets: ['@babel/preset-env']
        }))
        .pipe(dest('dist'))
}
module.exports = {
    script
}

html模板编译

将html模板文件编译成能够正常工做的html文件。

const { src, dest } = require('gulp')
// html模板使用的swig模板,所以引入相应转换插件
const swig = require('gulp-swig')
const page = () => {
    return src('src/*.html', { base: 'src' }) // base属性表示保存源文件从src文件夹开始的文件结构
        .pipe(swig({
            // 指定编译模板时使用的变量
            data: { name: 'test' }
        }))
        .pipe(dest('dist'))
}

module.exports = {
    page
}

图片压缩及字体拷贝

站点图片在使用前能够经过压缩方式,去除无用的头文件等,减少文件体积。

const { src, dest } = require('gulp')
const imagemin = require('gulp-image')
const image = () => {
    // ** 表示文件夹下的全部文件
    return src('src/assets/images/**', { base: 'src' }) // base属性表示保存源文件从src文件夹开始的文件结构
        .pipe(imagemin())
        .pipe(dest('dist'))
}

const font = () => {
    // ** 表示文件夹下的全部文件
    return src('src/assets/fonts/**', { base: 'src' }) // base属性表示保存源文件从src文件夹开始的文件结构
        // 因为字体文件夹下可能有图片,所以也压缩一下
        .pipe(imagemin())
        .pipe(dest('dist'))
}
module.exports = {
    image,
    font
}

清空编译文件

在编译以前,须要删除历史编译文件。

const del = require('del')
const clean = () => {
    // 返回的是promise对象,因此能够直接返回
    return del(['dist'])
}
module.exports = {
    clean
}

添加开发服务器

添加开发服务器将有助于开发阶段查看效果及调试。

const browserSync = require('browser-sync')
const bs = browserSync.create()
const serve = () => {
    bs.init({
        notify: false, // 去除站点启动成功提示
        port: 8080, // 指定站点端口号
        open: true, // 站点启动完成以后是否自动打开浏览器
        files: ['dist/**'], // 添加文件变化监听
        server: {
            // 指定站点文件根目录
            baseDir: 'dist',
            // 添加路由,解决非dist目录下的文件引用
            routes: {
                '/node_modules': 'node_modules'
            }
        }
    })
}
module.exports = {
    serve
}

监视变化

对文件变化添加监视,当开发时文件发生变化后,能够自动完成构建及浏览器刷新工做。

const { watch } = require('gulp')
const browserSync = require('browser-sync')
const bs = browserSync.create()
const serve = () => {
    // watch方法用于监视文件变化,当文件发生变化后,能够执行对应的构建
    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)
    bs.init({
        notify: false, // 去除站点启动成功提示
        port: 8080, // 指定站点端口号
        open: true, // 站点启动完成以后是否自动打开浏览器
        files: ['dist/**'], // 添加文件变化监听
        server: {
            // 指定站点文件根目录
            baseDir: ['dist', 'src'], // 提供数组,当dist中没有找到相应图片或者字体资源时,会自动去src目录下查找
            // 添加路由,解决非dist目录下的文件引用
            routes: {
                '/node_modules': 'node_modules'
            }
        }
    })
}
module.exports = {
    serve
}

其余依赖文件

<link rel="stylesheet" href="/node_moduls/bootstrap/dist/css/bootstrap.css" />
若是在模板文件依赖于node_modules文件夹下的文件,那么编译完成的html就由于dist目录下没有该文件而报错。
解决方案是利用useref插件和构建注释解决:

  • 首先添加构建注释:
<!--build:css assets/styles/vendor.css-->
    <link rel="stylesheet" href="/node_moduls/bootstrap/dist/css/bootstrap.css" />
    <!--endbuild-->

build和endbuild注释之间的全部资源文件将会被打包到 assets/styles/vendor.css,同时将上述代码替换为:
<link rel="stylesheet" href="assets/styles/vendor.css" />

  • 添加useref编译任务:
const { src, dest } = require('gulp')
const userefPlugin = require('gulp-useref')
const ifplugin = require('glup-if')
const uglify = require('glup-uglify')
const cleanCss = require('glup-clean-css')
const htmlmin = require('glup-htmlmin')
const useref = () => {
    return src('dist/*.html', { base: 'dist' })
        .pipe(userefPlugin({
            // 指定资源文件查找路径
            searchPath: ['dist', '.']
        }))
        // 分别压缩js,css,html代码
        .pipe(ifplugin(/\.js$/, uglify()))
        .pipe(ifplugin(/\.css$/, uglify()))
        .pipe(ifplugin(/\.html$/, uglify()))
        .pipe(dest('release'))
}
module.exports = {
    useref
}

整合

上面的多个章节,分别实现了自动化构建的不一样方面,整合在一块儿就能够实现一个自动化构建的功能,在整合时,须要完善下面两个方面:

  • 插件自动加载

gulp提供了gulp-load-plugins插件,能够自动加载全部已经npm install安装的gulp插件。

const plugin = require('gulp-load-plugins')
// 能够经过plugin.sass的方式引入gulp-sass插件
// plugin.sass
  • 整合零散任务

利用series和parallel能够将多个零散任务组装到一块儿,实现一个完整的构建工做流。

const compile = parallel(style, script, page)
const build = series(
    clean,
    parallel(
        series(compile, useref),
        image,
        font
    )
)
const develop = series(compile, serve)
module.exports = {
    compile,
    build,
    develop
}
相关文章
相关标签/搜索