大前端進階-工程化

概述

前端工程化是使用軟件工程的技術和方法來進行前端的開發流程、技術、工具、經驗等規範化,標準化,主要目的是爲了提升效率和下降成本。

目前,前端項目愈來愈複雜化和多元化,可是隨着變化而來的就是以下問題: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
}
相關文章
相關標籤/搜索