前端工程化是使用软件工程的技术和方法来进行前端的开发流程、技术、工具、经验等规范化,标准化,主要目的是为了提升效率和下降成本。
目前,前端项目愈来愈复杂化和多元化,可是随着变化而来的就是以下问题:javascript
前端工程化为上述问题提供了成熟的解决方案,做为使用者,能够更加关注业务逻辑的实现,也就提升了效率,下降了成本。php
建立项目的第一步就是利用脚手架工具建立项目模版,目前流行的框架均提供了脚手架工具,如react的create-react-app,vue的vue-cli。这些脚手架工具不只构建了统一的项目结构,同时提供了语法转换、模块化组件化、代码风格检查、单元测试、自动构建部署等方案。css
Yeoman是一种开源的脚手架工具,其相比vue-cli专门用于vue项目不一样,其更加灵活,能够基于不一样的generator生成不一样的项目。所以本篇文章将从Yeamon入手,探索如何搭建脚手架。html
使用Yeoman建立自定义脚手架,就是建立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
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
从自定义generator示例中能够看出,实现一个简单的脚手架主要是实现如下两个方面的内容:react
在实现自定义脚手架工具以前,得须要了解下面内容:git
在npm包的pakage.json文件中添加bin属性,bin的值是一个对象:
{ // 键表示命令,值表示在终端输入命令后执行的文件 "create-custom": "index.js" }
当项目中install这个包的时候,命令会注册到全局或者./node_modules/.bin/目录里。
在执行文件的开头须要加上`#!/usr/bin/env node`,不然不会被识别。
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) })
问题选项中的类型包含以下:
ejs是一种高效的嵌入式JavaScript模板引擎。
let ejs = require('ejs') ejs.render(` 选择的语言有<%= languages.join(',')%> `, { languages: ['php', 'javascript'] })
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>
和Yeoman不一样,plop是一个在项目内使用的,能够快速建立指定格式文件的脚手架工具,如在vue编程过程当中,每次建立.vue文件,均须要在文件中手动输入template,script,style三个节点,能够利用此工具一键生成文件,减小大量的重复工做。
使用步骤以下:
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文件 } ] // 执行操做 }) }
<template> <div class="{{name}}-container"> </div> </template> <script> export default { name: {{ name }} } </script> <style> .{{name}}-container { } </style>
在package.json文件的scripts属性下添加:"plop": "plop"
在命令行执行: npm run plop create-vue-file。
根据提示输入文件名和文件路径,最终会生成文件以下:
要想使用gulp,须要下面两个步骤:
在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提供了两个用于组合任务的函数:series
和parallel
,前者表示串行任务,即多个任务会依次执行。后者表示并行任务,即多个任务会同时执行。
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文件。
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" />
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 }