中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

詳解基于node.js的腳手架工具開發經歷

發布時間:2020-08-22 17:27:38 來源:腳本之家 閱讀:181 作者:張國鈺 欄目:web開發

前言

我們團隊的前端項目是基于一套內部的后臺框架進行開發的,這套框架是基于vue和ElementUI進行了一些定制化包裝,并加入了一些自己團隊設計的模塊,可以進一步簡化后臺頁面的開發工作。

這套框架拆分為基礎組件模塊,用戶權限模塊,數據圖表模塊三個模塊,后臺業務層的開發至少要基于基礎組件模塊,可以根據具體需要加入用戶權限模塊或者數據圖表模塊。盡管vue提供了一些腳手架工具vue-cli,但由于我們的項目是基于多頁面的配置進行開發和打包,與vue-cli生成的項目結構和配置有些不一樣,所以創建項目的時候,仍然需要人工去修改很多地方,甚至為了方便,直接從之前的項目copy過來然后進行魔改。表面上看問題不大,但其實存在很多問題:

  • 重復性工作,繁瑣而且浪費時間
  • copy過來的模板容易存在無關的代碼
  • 項目中有很多需要配置的地方,容易忽略一些配置點,進而埋坑
  • 人工操作永遠都有可能犯錯,建新項目時,總要花時間去排錯
  • 內部框架也在不停的迭代,人工建項目往往不知道框架最新的版本號是多少,使用舊版本的框架可能會重新引入一些bug

針對以上問題,我開發了一個腳手架工具,可以根據交互動態生成項目結構,自動添加依賴和配置,并移除不需要的文件。

接下來整理一下我的整個開發經歷。

基本思路

開始擼代碼之前,先捋一捋思路。其實,在實現自己的腳手架之前,我反復整理分析了vue-cli的實現,發現很多有意思的模塊,并從中借鑒了它的一些好的思想。

詳解基于node.js的腳手架工具開發經歷

vue-cli是將項目模板作為資源獨立發布在git上,然后在運行的時候將模板下載下來,經過模板引擎渲染,最后生成工程。這樣將項目模板與工具分離的目的主要是,項目模板負責項目的結構和依賴配置,腳手架負責項目構建的流程,這兩部分并沒有太大的關聯,通過分離,可以確保這兩部分獨立維護。假如項目的結構、依賴項或者配置有變動,只需要更新項目模板即可。

參照vue-cli的思路,我也將項目模板獨立發布到git上,然后通過腳手架工具下載下來,經過與腳手架的交互獲取新項目的信息,并將交互的輸入作為元信息渲染項目模板,最終得到項目的基礎結構。

工程結構

工程基于 nodejs 8.4 以及 ES6 進行開發,目錄結構如下

/bin # ------ 命令執行文件
/lib # ------ 工具模塊
package.json

下面的部分代碼需要你先對 Promise 有一定的了解才更好的理解。

使用commander.js開發命令行工具

nodejs內置了對命令行操作的支持,node工程下 package.json 中的 bin 字段可以定義命令名和關聯的執行文件。

{
 "name": "macaw-cli",
 "version": "1.0.0",
 "description": "我的cli",
 "bin": {
 "macaw": "./bin/macaw.js"
 }
}

經過這樣配置的nodejs項目,在使用 -g 選項進行全局安裝的時候,會自動在系統的 [prefix]/bin 目錄下創建相應的符號鏈接(symlink)關聯到執行文件。如果是本地安裝,這個符號鏈接會生成在 ./node_modules/.bin 目錄下。這樣做的好處是可以直接在終端中像執行命令一樣執行nodejs文件。關于 prefix ,可以通過 npm config get prefix 獲取。

hello, commander.js

在bin目錄下創建一個macaw.js文件,用于處理命令行的邏輯。

touch ./bin/macaw.js

接下來就要用到github上一位神級人物——tj ——開發的模塊commander.js 。commander.js可以自動的解析命令和參數,合并多選項,處理短參,等等,功能強大,上手簡單。具體的使用方法可以參見項目的README。

macaw.js 中編寫命令行的入口邏輯

#!/usr/bin/env node

const program = require('commander') // npm i commander -D

program.version('1.0.0')
	.usage('<command> [項目名稱]')
	.command('hello', 'hello')
	.parse(process.argv)

接著,在 bin 目錄下創建 macaw-hello.js ,放一個打印語句

touch ./bin/macaw-hello.js
echo "console.log('hello, commander')" > ./bin/macaw-hello.js

這樣,通過node命令測試一下

node ./bin/macaw.js hello

不出意外,可以在終端上看到一句話:hello, commander。

commander支持git風格的子命令處理 ,可以根據子命令自動引導到以特定格式命名的命令執行文件,文件名的格式是 [command]-[subcommand] ,例如:

  • macaw hello => macaw-hello
  • macaw init => macaw-init

定義init子命令

我們需要通過一個命令來新建項目,按照常用的一些名詞,我們可以定義一個名為 init 的子命令。

bin/macaw.js 做一些改動。

const program = require('commander')

program.version('1.0.0')
	.usage('<command> [項目名稱]')
	.command('init', '創建新項目')
	.parse(process.argv)

在bin目錄下創建一個 init 命令關聯的執行文件

touch ./bin/macaw-init.js

添加如下代碼

#!/usr/bin/env node

const program = require('commander')

program.usage('<project-name>').parse(process.argv)

// 根據輸入,獲取項目名稱
let projectName = program.args[0]

if (!projectName) { // project-name 必填
 // 相當于執行命令的--help選項,顯示help信息,這是commander內置的一個命令選項
 program.help() 
 return
}

go()

function go () {
	// 預留,處理子命令 
}

注意第一行 #!/usr/bin/env node 是干嘛的,有個關鍵詞叫Shebang,不了解的可以去搜搜看

project-name 是必填參數,不過,我想對 project-name 進行一些自動化的處理。

  • 當前目錄為空,如果當前目錄的名稱和 project-name 一樣,則直接在當前目錄下創建工程,否則,在當前目錄下創建以 project-name 作為名稱的目錄作為工程的根目錄
  • 當前目錄不為空,如果目錄中不存在與 project-name 同名的目錄,則創建以 project-name 作為名稱的目錄作為工程的根目錄,否則提示項目已經存在,結束命令執行。

根據以上設定,再對執行文件做一些完善

#!/usr/bin/env node

const program = require('commander')
const path = require('path')
const fs = require('fs')
const glob = require('glob') // npm i glob -D

program.usage('<project-name>')

// 根據輸入,獲取項目名稱
let projectName = program.args[0]

if (!projectName) { // project-name 必填
 // 相當于執行命令的--help選項,顯示help信息,這是commander內置的一個命令選項
 program.help() 
 return
}

const list = glob.sync('*') // 遍歷當前目錄
let rootName = path.basename(process.cwd())
if (list.length) { // 如果當前目錄不為空
 if (list.filter(name => {
 const fileName = path.resolve(process.cwd(), path.join('.', name))
 const isDir = fs.stat(fileName).isDirectory()
 return name.indexOf(projectName) !== -1 && isDir
 }).length !== 0) {
 console.log(`項目${projectName}已經存在`)
 return
 }
 rootName = projectName
} else if (rootName === projectName) {
 rootName = '.'
} else {
 rootName = projectName
}

go()

function go () {
	// 預留,處理子命令 
 	console.log(path.resolve(process.cwd(), path.join('.', rootName)))
}

隨意找個路徑下建一個空目錄,然后在這個目錄下執行咱們定義的初始化命令

node /[pathto]/macaw-cli/bin/macaw.js init hello-cli

正常的話,可以看到終端上打印出項目的路徑。

詳解基于node.js的腳手架工具開發經歷

使用download-git-repo下載模板

下載模板的工具用到另外一個node模塊download-git-repo ,參照項目的README,對下載工具進行簡單的封裝。

lib 目錄下創建一個 download.js

const download = require('download-git-repo')

module.exports = function (target) {
 target = path.join(target || '.', '.download-temp')
 return new Promise(resolve, reject) {
 // 這里可以根據具體的模板地址設置下載的url,注意,如果是git,url后面的branch不能忽略
 download('https://github.com:username/templates-repo.git#master',
 target, { clone: true }, (err) => {
 if (err) {
 reject(err)
 } else {
 // 下載的模板存放在一個臨時路徑中,下載完成后,可以向下通知這個臨時路徑,以便后續處理
 resolve(target)
 }
 })
 }
}

download-git-repo模塊本質上就是一個方法,它遵循node.js的CPS,用回調的方式處理異步結果。如果熟悉node.js的話,應該都知道這樣處理存在一個弊端,我把它進行了封裝,轉換成現在更加流行的Promise的風格處理異步。

再一次對之前的 macaw-init.js 進行修改

const download = require('./lib/download')

... // 之前的省略
function go () {
 download(rootName)
 .then(target => console.log(target))
 .catch(err => console.log(err))
}

下載完成之后,再將臨時下載目錄中的項目模板文件轉移到項目目錄中,一個簡單的腳手架算是基本完成了。轉移的具體實現方法就不細說了,可以參見node.js的API。你的node.js版本如果在8以下,可以用stream和pipe的方式實現,如果是8或者9,可以使用新的API——copyFile()或者 copyFileSync() 。

but...

這個世界并非我們想象的那么簡單。我們可能會希望項目模板中有些文件或者代碼可以動態處理。比如:

  • 新項目的 名稱版本號描述 等信息等,可以通過腳手架的交互進行輸入,然后將輸入插入到模板中
  • 項目模板并非所有文件都會用到,可以通過腳手架提供的選項移除掉那些無用的文件或者目錄。

對于這類情況,我們還需要借助其他工具包來完成。

使用inquirer.js處理命令行交互

對于命令行交互的功能,可以用inquirer.js 來處理。用法其實很簡單:

const inquirer = require('inquirer') // npm i inquirer -D

inquirer.prompt([
 {
 name: 'projectName',
 message: '請輸入項目名稱'
 }
]).then(answers => {
 console.log(`你輸入的項目名稱是:${answers.projectName}`)
})

prompt() 接受一個問題對象 的數據,在用戶與終端交互過程中,將用戶的輸入存放在一個 答案對象 中,然后返回一個 Promise ,通過 then() 獲取到這個答案對象。so easy!

接下來繼續對macaw-init.js進行完善。

// ...

const inquirer = require('inquirer')
const list = glob.sync('*')

let next = undefined
if (list.length) {
 if (list.filter(name => {
 const fileName = path.resolve(process.cwd(), path.join('.', name))
 const isDir = fs.stat(fileName).isDirectory()
 return name.indexOf(projectName) !== -1 && isDir
 }).length !== 0) {
 console.log(`項目${projectName}已經存在`)
 return
 }
 next = Promise.resolve(projectName)
} else if (rootName === projectName) {
 next = inquirer.prompt([
 {
 name: 'buildInCurrent',
 message: '當前目錄為空,且目錄名稱和項目名稱相同,是否直接在當前目錄下創建新項目?'
 type: 'confirm',
 default: true
 }
 ]).then(answer => {
 return Promise.resolve(answer.buildInCurrent ? '.' : projectName)
 })
} else {
 next = Promise.resolve(projectName)
}

next && go()

function go () {
 next.then(projectRoot => {
 if (projectRoot !== '.') {
 fs.mkdirSync(projectRoot)
 }
 return download(projectRoot).then(target => {
 return {
 projectRoot,
 downloadTemp: target
 }
 })
 })
}

如果當前目錄是空的,并且目錄名稱和項目名稱相同,那么就通過終端交互的方式確認是否直接在當前目錄下創建項目,這樣會讓腳手架更加人性化。

前面提到,新項目的名稱、版本號、描述等信息可以直接通過終端交互插入到項目模板中,那么再進一步完善交互流程。

// ...

// 這個模塊可以獲取node包的最新版本
const latestVersion = require('latest-version') // npm i latest-version -D

// ...

function go () {
 next.then(projectRoot => {
 if (projectRoot !== '.') {
 fs.mkdirSync(projectRoot)
 }
 return download(projectRoot).then(target => {
 return {
 name: projectRoot,
 root: projectRoot,
 downloadTemp: target
 }
 })
 }).then(context => {
 return inquirer.prompt([
 {
 name: 'projectName',
 	message: '項目的名稱',
 default: context.name
 }, {
 name: 'projectVersion',
 message: '項目的版本號',
 default: '1.0.0'
 }, {
 name: 'projectDescription',
 message: '項目的簡介',
 default: `A project named ${context.name}`
 }
 ]).then(answers => {
 return latestVersion('macaw-ui').then(version => {
 answers.supportUiVersion = version
 return {
 ...context,
 metadata: {
 ...answers
 }
 }
 }).catch(err => {
 return Promise.reject(err)
 })
 })
 }).then(context => {
 console.log(context)
 }).catch(err => {
 console.error(err)
 })
}

下載完成后,提示用戶輸入新項目信息。當然,交互的問題不僅限于此,可以根據自己項目的情況,添加更多的交互問題。inquirer.js強大的地方在于,支持很多種交互類型,除了簡單的 input ,還有 confirmlistpasswordcheckbox 等,具體可以參見項目的 README 。

然后,怎么把這些輸入的內容插入到模板中呢,這時候又用到另外一個簡單但又不簡單的工具包——metalsmith。

使用metalsmith處理模板

引用官網的介紹:

An extremely simple, pluggable static site generator.

它就是一個靜態網站生成器,可以用在批量處理模板的場景,類似的工具包還有Wintersmith、 Assemble 、Hexo。它最大的一個特點就是 EVERYTHING IS PLUGIN ,所以,metalsmith本質上就是一個膠水框架,通過黏合各種插件來完成生產工作。

給項目模板添加變量占位符

模板引擎我選擇handlebars。當然,還可以有其他選擇,例如ejs、 jade 、 swig 。

用handlebars的語法對模板做一些調整,例如修改模板中的 package.json

{
 "name": "{{projectName}}",
 "version": "{{projectVersion}}",
 "description": "{{projectDescription}}",
 "author": "Forcs Zhang",
 "private": true,
 "scripts": {
 "dev": "node build/dev-server.js",
 "start": "node build/dev-server.js",
 "build": "node build/build.js",
 "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
 "test": "npm run unit",
 "lint": "eslint --ext .js,.vue src test/unit/specs"
 },
 "dependencies": {
 "element-ui": "^2.0.7",
 "macaw-ui": "{{supportUiVersion}}",
 "vue": "^2.5.2",
 "vue-router": "^2.3.1"
 },
 ...
}

package.jsonnameversiondescription 字段的內容被替換成了handlebar語法的占位符,模板中其他地方也做類似的替換,完成后重新提交模板的更新。

實現腳手架給模板插值的功能

lib 目錄下創建 generator.js ,封裝metalsmith。

touch ./lib/generator.js

// npm i handlebars metalsmith -D
const Metalsmith = require('metalsmith')
const Handlebars = require('handlebars')
const rm = require('rimraf').sync

module.exports = function (metadata = {}, src, dest = '.') {
 if (!src) {
 return Promise.reject(new Error(`無效的source:${src}`))
 }
 
 return new Promise((resolve, reject) => {
 Metalsmith(process.cwd())
 .metadata(metadata)
 .clean(false)
 .source(src)
 .destination(dest)
 .use((files, metalsmith, done) => {
 	const meta = metalsmith.metadata()
 Object.keys(files).forEach(fileName => {
  const t = files[fileName].contents.toString()
  files[fileName].contents = new Buffer(Handlebars.compile(t)(meta))
 })
 	done()
 }).build(err => {
 	rm(src)
 	err ? reject(err) : resolve()
 })
 })
}

macaw-init.jsgo() 添加生成邏輯。

// ...
const generator = require('../lib/generator')

function go () {
 next.then(projectRoot => {
 // ...
 }).then(context => {
 // 添加生成的邏輯
 return generator(context)
 }).then(context => {
 console.log('創建成功:)')
 }).catch(err => {
 console.error(`創建失敗:${err.message}`)
 }) 
}

至此,一個帶交互,可動態給模板插值的腳手架算是基本完成了。

tips:墻裂推薦一下tj的另一個工具包: consolidate.js ,在vue-cli中發現的,感興趣的話可以去了解一下。

美化我們的腳手架

通過一些工具包,讓腳手架更加人性化。這里介紹兩個在vue-cli中發現的工具包:

ora - 顯示spinner

chalk - 給枯燥的終端界面添加一些色彩

這兩個工具包用起來不復雜,用好了會讓腳手架看起來更加高大上

用ora優化加載等待的交互

ora可以用在加載等待的場景中,比如腳手架中下載項目模板的時候可以使用,如果給模板插值生成項目的過程也有明顯等待的話,也可以使用。

以下載為例,對 download.js 做一些改良:

npm i ora -D
const download = require('download-git-repo')
const ora = require('ora')

module.exports = function (target) {
 target = path.join(target || '.', '.download-temp')
 return new Promise(resolve, reject) {
 const url = 'https://github.com:username/templates-repo.git#master'
 const spinner = ora(`正在下載項目模板,源地址:${url}`)
 spinner.start()
 download(url, target, { clone: true }, (err) => {
 if (err) {
 spinner.fail() // wrong :(
 reject(err)
 } else {
 spinner.succeed() // ok :)
 resolve(target)
 }
 })
 }
}

用chalk優化終端信息的顯示效果

chalk可以給終端文字設置顏色。

// ...
const chalk = require('chalk')
const logSymbols = require('log-symbols')

// ...

function go () {
 // ...
 next.then(/* ... */)
 /* ... */
 	.then(context => {
 // 成功用綠色顯示,給出積極的反饋
 console.log(logSymbols.success, chalk.green('創建成功:)'))
 console.log()
 console.log(chalk.green('cd ' + context.root + '\nnpm install\nnpm run dev'))
 }).catch(err => {
 // 失敗了用紅色,增強提示
 console.error(logSymbols.error, chalk.red(`創建失敗:${error.message}`))
 }) 
}

詳解基于node.js的腳手架工具開發經歷

根據輸入項移除模板中不需要的文件

有時候,項目模板中并不是所有文件都是需要的。為了保證新生成的項目中盡可能的不存在臟代碼,我們可能需要根據腳手架的輸入項來確認最終生成的項目結構,將沒用的文件或者目錄移除。比如vue-cli,創建項目時會詢問我們是否需要加入測試模塊,如果不需要,最終生成的項目代碼中是不包含測試相關的代碼的。這個功能如何實現呢?

實現的思路

我參考了git的思路,定義個 ignore 文件,將需要被忽略的文件名列在這個 ignore 文件里,配上模板語法。腳手架在生成項目的時候,根據輸入項先渲染這個 ignore 文件,然后根據 ignore 文件的內容移除不需要的模板文件,然后再渲染真正會用到的項目模板,最終生成項目。

詳解基于node.js的腳手架工具開發經歷

實現方案

根據以上思路,我先定義了屬于我們項目自己的 ignore 文件,取名為 templates.ignore

然后在這個 ignore 文件中添加需要被忽略的文件名。

{{#unless supportMacawAdmin}}
# 如果不開啟admin后臺,登錄頁面和密碼修改頁面是不需要的
src/entry/login.js 	
src/entry/password.js
{{/unless}}

# 最終生成的項目中不需要ignore文字自身
templates.ignore

然后在 lib/generator.js 中添加對 templates.ignore 的處理邏輯

// ...

const minimatch = require('minimatch') // https://github.com/isaacs/minimatch

module.exports = function (metadata = {}, src, dest = '.') {
 if (!src) {
 return Promise.reject(new Error(`無效的source:${src}`))
 }

 return new Promise((resolve, reject) => {
 const metalsmith = Metalsmith(process.cwd())
 .metadata(metadata)
 .clean(false)
 .source(src)
 .destination(dest)
	// 判斷下載的項目模板中是否有templates.ignore
 const ignoreFile = path.join(src, 'templates.ignore')
 if (fs.existsSync(ignoreFile)) {
 // 定義一個用于移除模板中被忽略文件的metalsmith插件
 metalsmith.use((files, metalsmith, done) => {
 const meta = metalsmith.metadata()
 // 先對ignore文件進行渲染,然后按行切割ignore文件的內容,拿到被忽略清單
 const ignores = Handlebars.compile(fs.readFileSync(ignoreFile).toString())(meta)
  .split('\n').filter(item => !!item.length)
 Object.keys(files).forEach(fileName => {
  // 移除被忽略的文件
  ignores.forEach(ignorePattern => {
  if (minimatch(fileName, ignorePattern)) {
  delete files[fileName]
  }
  })
 })
 done()
 })
 }
 metalsmith.use((files, metalsmith, done) => {
 const meta = metalsmith.metadata()
 Object.keys(files).forEach(fileName => {
 const t = files[fileName].contents.toString()
 files[fileName].contents = new Buffer(Handlebars.compile(t)(meta))
 })
 done()
 }).build(err => {
 rm(src)
 err ? reject(err) : resolve()
 })
 })
}

基于插件思想的metalsmith很好擴展,實現也不復雜,具體過程可參見代碼中的注釋。

總結

經過對vue-cli的整理,借助了很多node模塊,整個腳手架的實現并不復雜。

  • 將項目模板與腳手架工具分離,可以更好的維護模板和腳手架工具。
  • 通過commander.js處理命令行
  • 通過download-git-repo處理下載
  • 通過inquirer.js處理終端交互
  • 通過metalsmith和模板引擎將交互輸入項插入到項目模板中
  • 參考了git的ignore的思路,利用自定義的templates.ignore動態化的移除不必要的文件和目錄

以上就是我開發腳手架的主要經歷,中間還有很多不足的地方,今后再慢慢完善吧。

最后說一下,其實vue-cli能做的事情還有很多,具體的可以看看項目的README和源碼。關于腳手架的開發,不一定要完全造個輪子,可以看看另外一個很強大的模塊YEOMAN,借助這個模塊也可以很快的實現自己的腳手架工具。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

雷州市| 乐平市| 四子王旗| 苏尼特右旗| 宣武区| 兰坪| 云梦县| 海宁市| 博客| 瓦房店市| 清流县| 玛多县| 和林格尔县| 如东县| 保靖县| 同仁县| 大港区| 侯马市| 巢湖市| 玛沁县| 绵竹市| 马龙县| 大竹县| 昭通市| 珠海市| 武汉市| 莲花县| 临江市| 苗栗县| 西畴县| 泰宁县| 滦平县| 南江县| 浮梁县| 神木县| 桃源县| 吉木乃县| 菏泽市| 芜湖市| 江安县| 徐水县|