網(wǎng)架公司廠家seo技術(shù)優(yōu)化技巧
前段時間閱讀了 https://juejin.cn/post/7260144602471776311#heading-25 這篇文章;本文做一個梳理和筆記;
主要聚焦的知識點如下:
- 如何搭建腳手架工程
- 如何開發(fā)調(diào)試
- 如何處理命令行參數(shù)
- 如何實現(xiàn)用戶交互
- 如何拷貝文件夾或文件
- 如何動態(tài)生成文件
- 如何處理路徑
- 如何自動安裝依賴
step1:初始化工程
推薦使用pnpm搭建mono-repo風(fēng)格的工程;
mono-repo工程可以包含多個子工程,并且每個子工程都可以獨立編譯打包,并將打包的產(chǎn)物發(fā)成npm包;
這樣在同一個工程中,我們可以寫庫,也可以寫demo;
步驟如下:
- 執(zhí)行
pnpm init
命令,生成package.json文件; - 新建 pnpm-workspace.yaml 文件,添加如下配置:
packages: - 'packages/*' - 'demos/*'
配置后,聲明了 packages 和 demos 文件夾中子工程是同屬一個工作空間的,工作空間中的子工程編譯打包的產(chǎn)物都可以被其它子工程引用。
- 在packages文件夾下 新建zy-cli文件夾;
- cd 到 zy-cli文件夾下,運(yùn)行 pnpm init 初始化;
- zy-cli 下的 packages.json 中聲明 bin 命令 zy-script;
"bin": {"zy-script": "./bin/index.js"},
- 添加 bin文件夾,添加index.js文件;寫入如下代碼
#!/usr/bin/env node
console.log('hellow, zy-cli');
- demos 文件夾中存放使用腳手架的演示項目,我們先建一個app文件夾;
- 執(zhí)行
pnpm init
命令,生成package.json文件; - 在app中依賴 @zy/zy-cli ( 此處依賴名稱與zy-cli 的package.json 中 name一致 )
pnpm add @zy/zy-cli -F app
會在dependencies自動添加依賴;
- 并添加script指令 zy (與zy-cli中聲明的指令一致);
"scripts": {"zy": "zy-script"},"dependencies": {"@zy/zy-cli": "workspace:^"},
- 執(zhí)行pnpm -i 安裝依賴;
- 在app目錄下運(yùn)行:pnpm zy;成功輸出了
hellow, zy-cli
小節(jié):
到目前為止,我們成功創(chuàng)建了mono-repo風(fēng)格的項目結(jié)構(gòu);
packages > zy-cli 是我們腳手架工程,在bin中自定義了指令;
demos > app 是使用 zy-cli 腳手架的示例工程,利用pnpm的workspace,指定了工作區(qū)中zy-cli依賴,在script中自定義使用 zy-cli中聲明的命令;
整個工程結(jié)構(gòu)如下:
|-- my-cli|-- package.json|-- pnpm-lock.yaml|-- pnpm-workspace.yaml|-- demos| |-- app| |-- package.json|-- packages|-- zy-cli|-- package.json|-- bin|-- index.js
現(xiàn)在,我們思考一下,一個腳手架工程需要哪些模塊?
- 命令參數(shù)模塊
- 用戶交互模塊
- 文件拷貝模塊
- 動態(tài)文件生成模塊
- 自動安裝依賴模塊
接下來,我們一步一步實現(xiàn)他們;
step2:命令參數(shù)模塊
當(dāng)我們執(zhí)行命令的時候,經(jīng)常會帶一些參數(shù),如何獲取并利用這些參數(shù);
nodeJS 中 process 模塊,可以獲取當(dāng)前進(jìn)程相關(guān)的全局環(huán)境信息 - 命令行參數(shù),環(huán)境變量,命令運(yùn)行路徑等;
利用 progress.argv 獲取;
const process = require('process');
// 獲取命令參數(shù)
console.log(process.argv);
或者可以采用更便捷的方案 yargs 開源庫;
我們?yōu)閦y-cli 安裝 yargs:
pnpm add yargs --F zy-cli
注意:--F zy-cli 指的是指定給該工程安裝;-W 是全局安裝的意思
我們在zy-cli - bin - index.js 中寫入如下測試代碼
#!/usr/bin/env node
// 此處遵循CommonJS規(guī)范
const yargs = require('yargs');console.log('name', yargs.argv.name);
在demos - app 目錄下執(zhí)行 pnpm zy --name=zhang
打印輸出如下:
step3:創(chuàng)建子命令
我們在使用vue-cli的時候,都用過 vue creat app 之類的命令;creat就是子命令;
我們通過 yarg.command 來實現(xiàn);
yargs.command(cmd, desc, builder, handler)
- cmd:字符串,子命令名稱,也可以傳遞數(shù)組,如 ['create', 'c'],表示子命令叫 create,其別名是 c;
- desc:字符串,子命令描述信息;
- builder:子命令參數(shù)信息配置,比如可以設(shè)置參數(shù)(builder也可以是一個函數(shù)):
-
- alias:別名;
- demand:是否必填;
- default:默認(rèn)值;
- describe:描述信息;
- type:參數(shù)類型,string | boolean | number。
- handler: 函數(shù),可以在這個函數(shù)中專門處理該子命令參數(shù)。
下面我們定義一個creat命令:
#!/usr/bin/env node
const yargs = require('yargs');
console.log('name', yargs.argv.name);
yargs.command({// 字符串,子命令名稱,也可以傳遞數(shù)組,如 ['create', 'c'],表示子命令叫 create,其別名是 ccommand: 'create <name>',// 字符串,子命令描述信息;describe: 'create a new project',// 對象,子命令的配置項;builder也可以是一個函數(shù)builder: {name: {alias: 'n', // 別名demandOption: true, // 是否必填describe: 'name of a project', // 描述default: 'app' // 默認(rèn)}},// 函數(shù)形式的// builder: (yargs) => {// return yargs.option('name', {// alias: 'n',// demand: true,// describe: 'name of a project',// type: 'string'// })// },handler: (argv) => {console.log('argv', argv);}
});
我們運(yùn)行一下這個命令:pnpm zy create my-app
輸出如下:
step4:增加用戶交互
當(dāng)我們使用vue create xxx 的時候,命令行會出現(xiàn)選項式的交互,讓我們選擇配置;
這里我們也實現(xiàn)一下;使用 inquirer 庫;
運(yùn)行命令安裝 pnpm add inquirer@8.2.5 --F zy-cli
inquirer主要做了三件事情:
- 詢問用戶問題
- 獲取用戶輸入
- 校驗用戶輸入
const inquirer = require('inquirer');function inquirerPrompt(argv) {// 先獲取到了命令行中的nameconst { name } = argv;return new Promise((resolve, reject) => {inquirer.prompt([{type: 'input',name: 'name',message: '模板名稱',default: name,validate: function (val) {if (!/^[a-zA-Z]+$/.test(val)) {return "模板名稱只能含有英文";}if (!/^[A-Z]/.test(val)) {return "模板名稱首字母必須大寫"}return true;},},{type: 'list',name: 'type',message: '模板類型',choices: ['表單', '動態(tài)表單', '嵌套表單'],filter: function (value) {return {'表單': "form",'動態(tài)表單': "dynamicForm",'嵌套表單': "nestedForm",}[value];},},{type: 'list',message: '使用什么框架開發(fā)',choices: ['react', 'vue'],name: 'frame',}]).then(answers => {const { frame } = answers;if (frame === 'react') {inquirer.prompt([{type: 'list',message: '使用什么UI組件庫開發(fā)',choices: ['Ant Design',],name: 'library',}]).then(answers1 => {resolve({...answers,...answers1,})}).catch(error => {reject(error)})}if (frame === 'vue') {inquirer.prompt([{type: 'list',message: '使用什么UI組件庫開發(fā)',choices: [ 'Element'],name: 'library',}]).then(answers2 => {resolve({...answers,...answers2,})}).catch(error => {reject(error)})}}).catch(error => {reject(error)})})}exports.inquirerPrompt = inquirerPrompt;
其中 inquirer.prompt() 返回的是一個 Promise,我們可以用 then 獲取上個詢問的答案,根據(jù)答案再發(fā)起對應(yīng)的內(nèi)容。
在index.js 中引入并使用
#!/usr/bin/env nodeconst yargs = require('yargs');
const { inquirerPrompt } = require('./inquirer');// 命令配置
yargs.command({// 字符串,子命令名稱,也可以傳遞數(shù)組,如 ['create', 'c'],表示子命令叫 create,其別名是 ccommand: 'create <name>',// 字符串,子命令描述信息;describe: 'create a new project',// 對象,子命令的配置項;builder也可以是一個函數(shù)builder: {name: {alias: 'n', // 別名demandOption: true, // 是否必填describe: 'name of a project', // 描述default: 'app' // 默認(rèn)}},// 函數(shù)形式的// builder: (yargs) => {// return yargs.option('name', {// alias: 'n',// demand: true,// describe: 'name of a project',// type: 'string'// })// },handler: (argv) => {inquirerPrompt(argv).then((answers) => {console.log(answers);});}
}).argv;
我們運(yùn)行 pnpm zy create my-app 試試:
step5:文件夾拷貝
前幾節(jié)我們實現(xiàn)了一個可以讀取命令行的cli工程配置;
接下來,我們就要深入到cli腳手架的構(gòu)建;
首先是文件夾的拷貝。我們使用 copy-dir 庫來實現(xiàn)文件夾拷貝;
安裝:pnpm add copy-dir --F zy-cli
bin中創(chuàng)建copy.js,實現(xiàn)簡單的copy函數(shù),check函數(shù)
const copydir = require('copy-dir')
const fs = require('fs');function copyDir (from, to, option) {copydir.sync(from, to, option);
}/*** Checks if a directory or file exists at the given path.* @param {string} path - The path to check for existence.* @returns {boolean} - Returns true if the directory or file exists, false otherwise.*/
function checkMkdirExists(path){return fs.existsSync(path);
}exports.copyDir = copyDir;
exports.checkMkdirExists = checkMkdirExists;
這幾個函數(shù)比較簡單,但是主要難點在于使用;具體來說就是 from,to參數(shù);
先定個需求,我們運(yùn)行 creat 選擇 form類型
命令的時候,需要將 zy-cli > src > form
文件夾拷貝到 demos > app > src > <app-name>
中;
- 我們分析一下,如何獲取當(dāng)前模板的位置;也就是 copyDir 的 from 參數(shù);
__dirname 是用來動態(tài)獲取當(dāng)前文件模塊所屬目錄的絕對路徑。比如在 bin/index.js 文件中使用 __dirname ,__dirname 表示就是 bin/index.js 文件所屬目錄的絕對路徑 ~/Desktop/my-cli/zy-cli/bin。
使用 path.resolve( [from…], to )
將相對路徑轉(zhuǎn)成絕對路徑;
那我們模板的路徑就是:path.resolve( __dirname,'../src/form' )
;或者path.resolve( __dirname,'../src/${type}')
- 接下來,我們確定 copyDir 的 to 參數(shù);也就是目標(biāo)文件夾 <app-name>
我們運(yùn)行腳手架命令是在 app 目錄下;pnpm zy
執(zhí)行的是 app > packages.json
,所以在node腳本中,可以使用 process.cwd()
獲取文件路徑;
那我們拷貝的目標(biāo)路徑就是:path.resolve(process.cwd(), './src/${<app-name>}')
handler: (argv) => {inquirerPrompt(argv).then((answers) => {// 此處已經(jīng)獲取到了完整的模版參數(shù);開始進(jìn)行文件處理const { name, type, frame, library } = answers;// 判斷是否存在該項目文件夾const isMkdirExists = checkMkdirExists(path.resolve(process.cwd(),`./${name}`));if (isMkdirExists) {console.log( `${name}文件夾已經(jīng)存在`);} else {const templatePath = path.resolve(__dirname, `../src/${type}`);const targetPath = path.resolve(process.cwd(), `./${name}`);copyDir(templatePath, targetPath);}});}
運(yùn)行一下命令:pnpm zy create my-app
,選擇表單類型;回車,拷貝成功;
step6:目錄守衛(wèi)
如果我需要將文件拷貝到 app > pages > <name> 下,由于沒有pages目錄,命令會報錯;
我們簡單實現(xiàn)一個目錄守衛(wèi),幫我們創(chuàng)建不存在的目錄;
const copydir = require('copy-dir')
const fs = require('fs');function copyDir (from, to, option) {// 目錄守衛(wèi),不存在的目錄結(jié)構(gòu)會去創(chuàng)建mkdirGuard(to);copydir.sync(from, to, option);
}/*** Checks if a directory or file exists at the given path.* @param {string} path - The path to check for existence.* @returns {boolean} - Returns true if the directory or file exists, false otherwise.*/
function checkMkdirExists(path){return fs.existsSync(path);
}// 目錄守衛(wèi)
function mkdirGuard(target) {try {fs.mkdirSync(target, { recursive: true });} catch (e) {mkdirp(target)function mkdirp(dir) {if (fs.existsSync(dir)) { return true }const dirname = path.dirname(dir);mkdirp(dirname);fs.mkdirSync(dir);}}
}exports.copyDir = copyDir;
exports.checkMkdirExists = checkMkdirExists;
exports.mkdirGuard = mkdirGuard;
step7:文件拷貝
文件操作,主要使用 fs.readFileSync
讀取被拷貝的文件內(nèi)容,然后創(chuàng)建一個文件,再使用 fs.writeFileSync
寫入文件內(nèi)容;這兩個api都是比較熟悉的老朋友了;不做過多介紹;
我們定義一個 copyFile函數(shù):
function copyFile(from, to) {const buffer = fs.readFileSync(from);const parentPath = path.dirname(to);mkdirGuard(parentPath)fs.writeFileSync(to, buffer);
}exports.copyFile = copyFile;
使用方法與copyDir 類似,只不過需要精確到文件;這里就不演示了;
step8:動態(tài)文件生成
我們在定義腳手架的時候,會獲取很多類型的命令參數(shù),有些參數(shù)可能對我們模板文件產(chǎn)生影響。
例如,根據(jù)命令行中的name,動態(tài)修改packages中的name;
這里,我們需要依賴 mustache
;安裝:pnpm add mustache --F zy-cli
我們增加一個 renderTemplate 函數(shù):
接受動態(tài)模板的path路徑,data:動態(tài)模版的配置數(shù)據(jù);
Mustache.render(str, data) 接受動態(tài)模版和配置數(shù)據(jù);
Mustache.render('<span>{{name}}</span>',{name:'張三'})
const Mustache = require('mustache');
function renderTemplate(path, data = {}) {const str = fs.readFileSync(path, { encoding: 'utf8' })return Mustache.render(str, data);
}
exports.renderTemplate = renderTemplate;
再定義一個copyTemplate 函數(shù):
path.extname
獲取文件擴(kuò)展名,如果不是tpl類型的,直接當(dāng)做文件處理;
function copyTemplate(from, to, data = {}) {if (path.extname(from) !== '.tpl') {return copyFile(from, to);}const parentToPath = path.dirname(to);// 目錄守衛(wèi)mkdirGuard(parentToPath);// 寫入文件fs.writeFileSync(to, renderTemplate(from, data));
}
在index.js中試驗一下:
const templatePath = path.resolve(__dirname, `../src/${type}/packages.tpl`);
const targetPath = path.resolve(process.cwd(), `./${name}/packages.json`);
copyTemplate(templatePath, targetPath, {name: name})
{"name": "{{name}}","version": "1.0.0","description": "","main": "index.js","scripts": {},"keywords": [],"author": "","license": "ISC"
}
運(yùn)行完創(chuàng)建命令后,成功生成packages.json 文件,并且將 name字段替換成功;
擴(kuò)展:mustache 一些用法補(bǔ)充:
基礎(chǔ)綁定:
Mustache.render('<span>{{name}}</span>',{name:'張三'})
綁定子屬性
Mustache.render('<span>{{ifno.name}}</span>', { ifno: { name: '張三' } })
循環(huán)渲染
// {{#key}} {{/key}} 開啟和結(jié)束循環(huán)
Mustache.render('<span>{{#list}}{{name}}{{/list}}</span>',{list: [{ name: '張三' },{ name: '李四' },{ name: '王五' },]}
)
循環(huán)渲染 + 二次處理
Mustache.render('<span>{{#list}}{{info}}{{/list}}</span>',{list: [{ name: '張三' },{ name: '李四' },{ name: '王五' },],info() {return this.name + ',';}}
)
條件渲染
// 使用 {{#key}} {{/key}} 語法 和 {{^key}} {{/key}} 語法來實現(xiàn)條件渲染,
// 當(dāng) key 為 false、0、[]、{}、null,既是 key == false 為真,
// {{#key}} {{/key}} 包裹的內(nèi)容不渲染,
// {{^key}} {{/key}} 包裹的內(nèi)容渲染
Mustache.render('<span>{{#show}}顯示{{/show}}{{^show}}隱藏{{/show}}</span>',{show: false}
)
step9:實現(xiàn)自動安裝依賴
我們在選擇完框架和UI庫的時候,可以幫助目標(biāo)項目自動安裝依賴;
我們使用 node 中提供的 child_process 子進(jìn)程來實現(xiàn);
- child_process.exec(command, options, callback)
-
- command:命令,比如 pnpm install
- options:參數(shù)
-
-
- cwd:設(shè)置命令運(yùn)行環(huán)境的路徑
- env:環(huán)境變量
- timeout:運(yùn)行執(zhí)行現(xiàn)在
-
-
- callback:運(yùn)行命令結(jié)束回調(diào),(error, stdout, stderr) =>{ },執(zhí)行成功后 error 為 null,執(zhí)行失敗后 error 為 Error 實例,stdout、stderr 為標(biāo)準(zhǔn)輸出、標(biāo)準(zhǔn)錯誤,其格式默認(rèn)是字符串。
我們定義一個 manager 函數(shù);
const path = require('path');
const { exec } = require('child_process');// 組件庫映射,前面是用戶輸入/選擇,后面是目標(biāo)安裝的組件庫
const LibraryMap = {'Ant Design': 'antd','iView': 'view-ui-plus','Ant Design Vue': 'ant-design-vue','Element': 'element-plus',
}function install(cmdPath, options) {// 用戶選擇的框架 和 組件庫const { frame, library } = options;// 安裝命令const command = `pnpm add ${frame} && pnpm add ${LibraryMap[library]}`return new Promise(function (resolve, reject) {// 執(zhí)行安裝命令exec(command,{// 命令執(zhí)行的目錄cwd: path.resolve(cmdPath),},function (error, stdout, stderr) {console.log('error', error);console.log('stdout', stdout);console.log('stderr', stderr)})})
}
exports.install = install;
使用:
// 傳入當(dāng)前進(jìn)程的目錄,以及用戶選擇的配置
install(process.cwd(), answers)
試驗一下:pnpm create xxxx
;成功安裝;
但是安裝過程沒有進(jìn)度展示;我們使用 ora 來豐富安裝加載動畫;
安裝:pnpm add ora@5.4.1 --F zy-cli
使用:
const path = require('path');
const { exec } = require('child_process');
const ora = require("ora");// 組件庫映射,前面是用戶輸入/選擇,后面是目標(biāo)安裝的組件庫
const LibraryMap = {'Ant Design': 'antd','iView': 'view-ui-plus','Ant Design Vue': 'ant-design-vue','Element': 'element-plus',
}function install(cmdPath, options) {// 用戶選擇的框架 和 組件庫const { frame, library } = options;// 串行安裝命令const command = `pnpm add ${frame} && pnpm add ${LibraryMap[library]}`return new Promise(function (resolve, reject) {const spinner = ora();spinner.start(`正在安裝依賴,請稍等`);// 執(zhí)行安裝命令exec(command,{// 命令執(zhí)行的目錄cwd: path.resolve(cmdPath),},function (error) {if (error) {reject();spinner.fail(`依賴安裝失敗`);return;}spinner.succeed(`依賴安裝成功`);resolve()})})
}
exports.install = install;
再次執(zhí)行,已經(jīng)有狀態(tài)提示了;
step10:推送到私有npm倉庫
使用verdaccio搭建私有npm倉庫的步驟本文不贅述,可以參考這篇文章;搭建自己的私有npm庫
// TODO 部署過程中使用docker-compose,遇到一些問題,預(yù)計單獨開一篇文章去記錄;
假設(shè)我們已經(jīng)有了npm私庫;ip:http://xxxxxx:4873/
我們使用 nrm 去管理 npm 的倉庫地址
// 全局安裝
npm install -g nrm// 查看所有的倉庫
nrm ls
// 切換倉庫
nrm use <name>
// 添加倉庫
nrm add <name> <address>
推送之前,我們需要修改 packages.json 中的信息:
{"name": "@zy/zy-cli","version": "1.0.0","description": "","main": "index.js","bin": {"zy-script": "./bin/index.js"},"scripts": {"test": "echo \"Error: no test specified\" && exit 1"},// 規(guī)定上傳到npm包中的文件"files": ["bin","src"],"keywords": [],"author": "","license": "ISC","dependencies": {"copy-dir": "^1.3.0","inquirer": "8.2.5","mustache": "^4.2.0","ora": "5.4.1","yargs": "^17.7.2"}
}
推送:
pnpm publish --registry http://xxxxx:4873/
刷新我們的 vedaccio,已經(jīng)存在這個包了
使用:
我們在Desktop中新建一個空白文件夾;
mkdir cli-test
cd cli-test
pnpm init
nrm use zy
pnpm i @zy/zy-cli
此時,我們的cli-test項目已經(jīng)成功安裝了私有npm倉庫的 zy-cli 項目
在packages.json 中添加命令
"scripts": {"zy-script": "zy-script"},
執(zhí)行 pnpm zy-script create Myapp
成功安裝所有依賴并拷貝文件;
總結(jié):
- 我們搭建了一個mono-repo風(fēng)格的工程;包含了一個zy-cli腳手架工程,和demos-app的測試工程;
- zy-cli實現(xiàn)了用戶交互式的命令行,命令行參數(shù)獲取,文件拷貝,動態(tài)文件生成,自動安裝依賴;
- 我們將zy-cli推送到了npm私有倉庫上,并另開了一個工程,切換私庫源,成功安裝并且運(yùn)行;
展望:
目前初步實現(xiàn)了mono-repo工程,還需要添加統(tǒng)一的publish腳本,包含版本自增等;
cli 腳手架不需要打包,所以還需要為這個工程添加一個 組件庫,工具函數(shù)庫等類型的包;