沒有前端工程化遇到的問題
- 使用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'])
}