前端工程化-01-Yeoman-Grunt

沒有前端工程化遇到的問題

  • 使用ES6+新特性,但是有兼容問題
  • 使用Less/Sass/PostCss增強CSS的變成性,運行環境不能直接支持
  • 使用或快畫的方式提高項目的可維護性,運行環境不能直接支持
  • 部署上線前需要手動壓縮代碼及資源文件
  • 部署過程中需要手動上傳代碼到服務器
  • 多人協作無法硬性統一大家的代碼風格,從倉庫中pull回來的代碼質量無法保證
  • 開發是需要等待後端服務接口提前完成

工程化表現

一切以提高效率、降低成本、質量保證爲目的的手段都屬於「工程化」

graph LR
創建項目-->編碼
編碼-->預覽/測試
預覽/測試-->提交
提交-->部署
部署-->編碼

工程化不等於某個工具

工程化的核心是對項目整體的規劃和架構,工具只是落地規劃和架構的一種手段

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hYlfGtv4-1591460280651)(http://oss.ahh5.com/ahh5/md/202020200529221827.png)]

工程化包含

  • 腳手架工具開發
  • 自動化構建系統
  • 模塊化打包
  • 項目代碼規範
  • 自動化部署

腳手架工具概要

腳手架的本質就是自動的創建項目基礎結構、提供項目規範和約定,腳手架工具可以快速的搭建特定項目的骨架。

約定

  • 相同的組織結構
  • 相同的開發範式
  • 相同的模塊化依賴
  • 相同的攻擊配置
  • 相同的基礎代碼

常用的腳手架工具

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

相同點

根據提供信息自動創建對應的項目基礎結構,但一般適用於自身所服務框架的項目

Yeoman(老牌、強大、通用)

不針對於某一框架相對常用腳手架較爲靈活。Yeoman可以搭配不同的generator創建不同的項目

缺點:過於通用不夠專注

基礎使用

安裝

yarn global add yo

安裝對應的generator ==> generator-node

yarn global add generator-node

通過yo運行generator

yo node 
# 輸入項目名字
# 輸入項目名字
# 輸入項目主頁
# 輸入作者
# 輸入郵箱
# 輸入主頁
# 輸入關鍵詞

Yeoman sub Generator

可以通過生成器的子集生成一些文件。例如eslint README

生成一個node cli

# 創建cli
yo node:cli  
# 安裝新增加的依賴
yarn  
# 鏈接到全局
yarn link "yo-test-learn" 
# 或者
npm link yo-test-learn
# 輸入下面命令即可看到
yo-test-learn --help 

Yeoman 使用步驟

  • 明確需求
  • 找到合適的 Generator
  • 全局範圍安裝找到的 Generator
  • 通過運行 Yo 運行對應的 Generator
  • 通過交互命令交互填寫選項
  • 生成你所需要的項目結構

自定義Generator

基於Yeoman 搭建自己的腳手架

Generator 基本結構

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-msZgVvsb-1591460280657)(http://oss.ahh5.com/ahh5/md/202020200530230529.png)]

提供多個 sub generator 需要在app同級目錄下創建一個新的目錄

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-tjkNyYoP-1591460280660)(http://oss.ahh5.com/ahh5/md/202020200530230749.png)]

# 創建一個文件夾
mkidr generator-hello
# 進入目錄
cd generator-hello
# 創建package.json
yarn init
# 安裝 yeoman-generator 模塊
yarn add yeoman-generator

創建 /generators/app/index.js

  • 此文件爲generator的核心入口
  • 需要導出一個繼承自 Yeoman Generator 的類型
  • Yeoman Generator 工作時會自動調用我們在此類型中定義的一些生命週期方法
  • 在這個文件中調用父類中的一些方法實現一些功能比如文件寫入
const Generator = require('yeoman-generator')

module.exports = class extends Generator {
    // yeoman 自動在生成文件中調用此方法
    writing() {
        // 通過文件讀寫方式向目標目錄寫入文件
        this.log('hello')
        // this.destinationPath 目前目錄路徑
        this.fs.write(
            this.destinationPath('temp.txt'),
            Math.random().toString()
        )
    }
}

通過 npm link 將模塊安裝到全局

找一個空目錄運行 yo hello

.
└── temp.txt

根據模板創建文件

在app目錄下創建一個templates目錄

這是一個模板文件,內部使用EJS模板標記輸入數據

app/templates/foo.txt

<%= title %>
<% if(success){ %>

    成功纔會看到我

<% } %>

app/index.js

const Generator = require('yeoman-generator')

module.exports = class extends Generator{
    // yeoman 自動在生成文件中調用此方法
    writing(){
        // 模板文件路徑
        const tmpl = this.templatePath('foo.txt')
        // 輸出目標路徑
        const output = this.destinationPath('foo.txt')
        // 模板數據上下文
        const context = {
            title:"hello word",
            success : true
        }
        // 輸出模板文件
        this.fs.copyTpl(tmpl,output,context)
    }
}

完成目錄結構

├── generators
│   └── app
│       ├── index.js
│       └── templates
│           └── foo.txt
├── package-lock.json
├── package.json
└── yarn.lock

找一個空目錄運行 yo hello 即可看到輸出的 foo.txt

內容如下

hello word

    成功纔會看到我

接收用戶輸入數據

app/index.js 暴露出的類添加如下方法

prompting() {
    // 在此方法可以調用父類的 prompt() 方法對用戶命令行詢問
    return this.prompt([{
            type: 'input',
            name: 'name',
            message: 'your project name',
            default: this.appname //當前生成目錄文件夾的名字
        }])
        .then(answers => {
            // answers => { name : 'user input value' }
            this.answers = answers
        })
}

修改 writing

writing() {
    // 模板文件路徑
    const tmpl = this.templatePath('foo.txt')
    // 輸出目標路徑
    const output = this.destinationPath('foo.txt')
    // 模板數據上下文
    const context = {
        // 此處title 接收用戶輸入的值
        title: this.answers.name,
        success: true
    }
    // 輸出模板文件
    this.fs.copyTpl(tmpl, output, context)
}

執行 yo hello
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-xWzvFQFE-1591460280662)(http://oss.ahh5.com/ahh5/md/202020200531112617.png)]

輸出文件內容如下

My project

    成功纔會看到我

自己實現一個 Vue Generator

基礎vue 構建我選擇的還是cli,基於上面在做一些自己的配置就好了

# 創建文件夾
mkdir generator-zzy-vue
# 進入文件夾
cd generator-zzy-vue  
# 初始化項目
yarn init   
# 添加yeoman依賴   
yarn add  yeoman-generator 

手動創建一些文件

基本結構如下

├── generators
│   └── app
│       ├── index.js
│       └── templates
├── package.json
└── yarn.lock

創建一個vue模板

vue create tmp-project
rm -rf node_modules
rm -rf .git
rm yarn.lock

目錄結構如下

├── .browserslistrc
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── README.md
├── babel.config.js
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html
└── src
    ├── App.vue
    ├── assets
    │   └── logo.png
    ├── components
    │   └── HelloWorld.vue
    ├── main.js
    ├── router
    │   └── index.js
    ├── store
    │   └── index.js
    └── views
        ├── About.vue
        └── Home.vue

修改README.md

# <%= name %>

修改 /public/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="favicon.ico">
    <title><%= name %></title>
</head>

<body>
    <noscript>
        <strong>We're sorry but <%= name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
</body>

</html>

複製到generators/app/templates中

app/index.js

const Generator = require('yeoman-generator')

module.exports = class extends Generator {
    // yeoman 在詢問用戶環節會自動調用此方法
    prompting() {
        return this.prompt([{
                type: "input",
                name: 'name',
                message: "Your project name",
                default: this.appname
            }])
            .then(answers => {
                this.answers = answers
            })
    }
    // yeoman 自動在生成文件中調用此方法
    writing() {
        const templates = [".browserslistrc", ".editorconfig", ".eslintrc.js", ".gitignore", "README.md", "babel.config.js", "package.json", "public/favicon.ico", "public/index.html", "src/App.vue", "src/assets/logo.png", "src/components/HelloWorld.vue", "src/main.js", "src/router/index.js", "src/store/index.js", "src/views/About.vue", "src/views/Home.vue"]
        templates.forEach(filePath => {
            this.fs.copyTpl(
                this.templatePath(filePath),
                this.destinationPath(filePath),
                this.answers
            )
        })
    }
}

關於templates文件名獲取, 可以通過node腳本實現

const fs = require('fs');
const path = require('path');
const _filePath = path.resolve('./templates');
const outFilePath = path.resolve('./arr');
let filePathArr = []
getFileRecursively(_filePath)

function getFileRecursively(filePath) {
    const files = fs.readdirSync(filePath)
    files.forEach(filename => {
        const filedir = path.join(filePath, filename);
        const stats = fs.statSync(filedir)
        if (stats.isFile()) {
            filedir.includes('.DS_Store') || filePathArr.push(filedir.replace(_filePath + '/', ''))
        }
        if (stats.isDirectory()) {
            getFileRecursively(filedir);
        }
    })
}
fs.writeFileSync(outFilePath, JSON.stringify(filePathArr))

注意不要將此方法放入 app/index.js 而是通過該腳本獲取並輸出到一個新的文件中。後面在手動複製到app/index.js中。因爲不能確定 yeoman 的執行環境,以及不需要用戶每次執行都去重新遞歸遍歷所有文件

發佈一個自己的npm包

  • 建立一個公開倉庫推薦github
  • 創建一個npm 用戶
  • npm login
  • npm publish

plop

一個小而美的腳手架工具,一般不會獨立去使用,而是集成到項目之中區創建同類型的文件使用

plop基本使用

  • 將plop模塊作爲項目開發依賴安裝
  • 在項目根目錄中創建一個plopfile.js文件
  • 在plopfile.js文件中定義腳手架任務
  • 編寫用於生成特定類型文件的模板
  • 通過Plop提供的Cli運行腳手架任務

自動化構建

自動化通過機器代替手工完成某些操作,

自動化構建就是將源代碼自動化構建爲生成代碼或者程序。

常見的自動化構建工具

  • Grunt

    最早的自動化構建工具,但因爲通過臨時文件工作,所以工作效率較慢。

  • Gulp

    使用頻率較高,通過操作內存實現自動化構建,相對於Grunt速度快了很多且默認支持多任務

  • Fls

    百度開源產品,繼承項目常用自動化構建流程,例如資源加載、模塊化開發、代碼部署、性能優化,缺點不夠靈活。

Grunt

基本使用

# 初始化項目
yarn init --yes   
# 添加grunt模塊
yarn add grunt --dev

根目錄下創建gruntfile.js文件

  • Grunt入口文件
  • 用於定義一些需要 Grunt 自動執行的任務
  • 需要導出一個函數
  • 函數需要接收一個grunt的形參,內部提供了一些創建任務是可以用到的 API
module.exports = grunt => {
    // 註冊的任務  第一個參數爲任務名稱  第二個參數爲任務描述(可以省略)  第三個是執行代碼
    grunt.registerTask('foo', '任務描述', () => {
        console.log('hello word')
    })
    // 如果任務名稱爲default 那麼yarn grunt 會默認執行
    grunt.registerTask('default', () => {
        console.log('grunt default task')
    })
}
# 運行grunt 不加參數執行默認任務
yarn grunt

# 指定任務執行
yarn grunt foo

# 查看任務描述
yarn grunt --help

default實際工作中是執行多個task的默認任務例如

module.exports = grunt => {
    grunt.registerTask('foo', 'foo任務描述', () => {
        console.log('hello foo')
    })
    grunt.registerTask('bar', 'bar任務描述', () => {
        console.log('hello bar')
    })
    grunt.registerTask('default', ['foo', 'bar'])
}

此時執行 yarn grunt 會執行 foo bar 兩個任務

異步任務

module.exports = grunt => {
    grunt.registerTask('async-task', 'async-task任務描述', function() {
        // 需要定義一個done 值爲this.async() 標記此任務爲異步任務
        const done = this.async()
        setTimeout(() => {
            console.log('async-task workding')
            // 異步任務執行結束後需要通過 done() 觸發程序截止
            done()
        }, 1000)
    })
}

標記任務失敗

在函數內部 return false

module.exports = grunt => {
    grunt.registerTask('bad', 'bad任務描述', () => {
        console.log('This is a bad task')
        return false
    })
}

如果在任務列表中,則後續任務不會執行

module.exports = grunt => {
    grunt.registerTask('bad', 'bad任務描述', () => {
        console.log('This is a bad task')
        return false
    })
    grunt.registerTask('test', 'test任務描述', () => {
        console.log('This is a test task')
        return false
    })
    grunt.registerTask('default', ['bad', 'test'])
}
// 此時執行 yarn grunt 則會拋出 warnings 警告且 test不會執行

如果採用強制執行方式 --force 則所有的任務都會執行

yarn grunt --force

異步任務失敗標記

module.exports = grunt => {
    grunt.registerTask('async-task', 'async-task任務描述', function() {
        const done = this.async()
        setTimeout(() => {
            console.log('async-task workding')
            // done 傳入一個實參 false
            done(false)
        }, 1000)
    })
}

配置選項方法

基本用法

module.exports = grunt => {
    // 初始化config
    grunt.initConfig({
        foo: 'test'
    })
    grunt.registerTask('foo', 'foo任務描述', () => {
        // 獲取config foo的鍵值
        const config_foo = grunt.config('foo')
        console.log( `hello ${config_foo}` )
        // 輸出 hello test
    })
}

多目標模式

通過配置多個目標, 執行多個目標

module.exports = grunt => {
    grunt.initConfig({
        // 定義build
        build: {
            // 參數配置不會作爲目標執行
            options: {
                test: false
            },
            // 定義兩個目標
            css: '1',
            js: '2'
        }
    })
    // 通過 registerMultiTask 方法定義
    grunt.registerMultiTask('build', function() {
        // 拿出執行目標值與值
        const {
            target,
            data,
        } = this
        // 獲取所有配置
        const { test } = this.options()
        console.log( `build ${target} ${data} ` )
        console.log(` test ${test} `)
    })
}

插件使用

安裝一個文件清除的插件

yarn add grunt-contrib-clean -D 
module.exports = grunt => {
    grunt.initConfig({
        // 定義clean
        clean: {
            // 定義一個目標 清除temp 下的所有文件
            temp:'temp/**'
        }
    })
    // 清除指定文件任務 需要多個目標
    grunt.loadNpmTasks('grunt-contrib-clean')
}

常用插件

編譯sass

# 安裝 grunt-sass 及 sass 模塊
yarn add grunt-sass sass -D 
// 引入sass 模塊
const sass = require('sass')
module.exports = grunt => {
    grunt.initConfig({
        sass: {
            // 定義配置文件
            options:{
                // 定義處理方法
                implementation:sass,
                // 開啓sourceMap文件
                sourceMap:true
            },
            // 定義個目標
            main: {
                // 定義文件
                files:{
                    // 輸出路徑   輸入路徑
                    'dist/css/main.css':'src/scss/main.scss'
                }
            }
        }
    })
    grunt.loadNpmTasks('grunt-sass')
}

npm grunt插件自動導出

# 安裝插件
yarn add load-grunt-tasks -D 
// 引入loadGruntTasks
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
    // 將grunt傳入loadGruntTasks 實現掛載所有的grunt模塊
    loadGruntTasks(grunt)
}

編譯es6語法

yarn add grunt-babel @babel/core @babel/preset-env -D 
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
    grunt.initConfig({
        babel: {
            // 定義配置文件
            options: {
                presets:['@babel/preset-env'],
                // 開啓sourceMap文件
                sourceMap: true
            },
            // 定義個目標
            main: {
                // 定義文件
                files: {
                    // 輸出路徑   輸入路徑
                    'dist/js/main.js': 'src/js/main.js'
                }
            }
        }
    })
    loadGruntTasks(grunt)
}

執行 yarn babel

修改自動更新

yarn add grunt-contrib-watch -D 
const loadGruntTasks = require('load-grunt-tasks')
const sass = require('sass')
module.exports = grunt => {
    grunt.initConfig({
        sass: {
            // 定義配置文件
            options: {
                // 定義處理方法
                implementation: sass,
                // 開啓sourceMap文件
                sourceMap: true
            },
            // 定義個目標
            main: {
                // 定義文件
                files: {
                    // 輸出路徑   輸入路徑
                    'dist/css/main.css': 'src/scss/main.scss'
                }
            }
        },
        babel: {
            // 定義配置文件
            options: {
                presets: ['@babel/preset-env'],
                // 開啓sourceMap文件
                sourceMap: true
            },
            // 定義個目標
            main: {
                // 定義文件
                files: {
                    // 輸出路徑   輸入路徑
                    'dist/js/main.js': 'src/js/main.js'
                }
            }
        },
        watch: {
            js: {
                files: ['src/js/*.js'],
                tasks: ['babel']
            },
            css: {
                files: ['src/scss/*.scss'],
                tasks: ['sass']
            }

        },
    })
    loadGruntTasks(grunt)
    grunt.registerTask('default', ['sass', 'babel','watch'])
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章