前端工程化是使用軟件工程的技術和方法來進行前端的開發流程、技術、工具、經驗等規範化,標準化,主要目的是爲了提升效率和下降成本。
目前,前端項目愈來愈複雜化和多元化,可是隨着變化而來的就是以下問題: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 }