国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

寫(xiě)給5年前端妹子的三萬(wàn)字腳手架教程

共 45265字,需瀏覽 91分鐘

 ·

2023-10-26 15:54


夜,一陣刺耳的手機(jī)鈴聲把我吵醒,我迅速的把手機(jī)按成靜音,看下屏幕,是我?guī)啄昵皫н^(guò)的一個(gè)妹子的電話,看著熟睡的老婆孩子,我躡手躡腳地走到廚房接通了電話。

一接通,就傳來(lái)一陣哭泣聲:“師傅,我找不到工作,找了好久!”

我納悶地問(wèn)道:“你不是有5年前端工作經(jīng)驗(yàn),怎么會(huì)找不到工作?”

“師傅,你走了后,我就一直呆在這家公司,做的都是增刪改查業(yè)務(wù),面試時(shí)他們一聽(tīng)這些就不感興趣,我都不知道怎么介紹我的項(xiàng)目經(jīng)歷?!?/p>

到此我也大致明白了是怎么一回事,安慰了一會(huì)兒,告訴她回頭給你想想辦法,便掛斷電話了。

回到床上久久不能入眠,回想當(dāng)初自己第一次換工作時(shí)遇到過(guò)同樣的問(wèn)題。那時(shí)候工作剛滿三年,起初在面試中也是干巴巴地講述一些增刪改查的項(xiàng)目經(jīng)歷。隨著幾次面試的失敗,開(kāi)始講述一些復(fù)雜表單表格的開(kāi)發(fā)經(jīng)驗(yàn),僥幸跳槽加薪成功。

但是5年工作經(jīng)驗(yàn)還講這些就不適合了,這位5年的妹子應(yīng)該在面試中體現(xiàn)出擁有增刪改查業(yè)務(wù)的最佳實(shí)踐,加上5年前端應(yīng)該具備一些前端工程方面的能力。

于是,第二天打電話告訴妹子:“你先把5年增刪改查業(yè)務(wù)的開(kāi)發(fā)經(jīng)驗(yàn)歸納整理成一份最佳實(shí)踐的開(kāi)發(fā)模板,然后我教你寫(xiě)一個(gè)腳手架,把這些開(kāi)發(fā)模板放在腳手架中,其他項(xiàng)目通過(guò)腳手架來(lái)使用這些模板,提高開(kāi)發(fā)效率。”

妹子遲疑地問(wèn)道:“腳手架,一聽(tīng)就很難,好不好學(xué)呢?”

“放心,腳手架很簡(jiǎn)單的,腳手架其實(shí)是一個(gè)使用 js 編寫(xiě)的 Node.js 命令行工具(CLI),里面也都是js代碼,只不過(guò)用 Node.js 來(lái)執(zhí)行的,還用了一些node的API而已,你先去整理最佳實(shí)踐的模板,腳手架我教你寫(xiě),包教包會(huì)。”

你會(huì)學(xué)到什么

其實(shí)腳手架并不實(shí)現(xiàn),難的是最佳實(shí)踐的整理和沉淀。本文不會(huì)涉及到最佳實(shí)踐方面的內(nèi)容,只是教會(huì)你如何實(shí)現(xiàn)一個(gè)最基礎(chǔ)的腳手架,以此作為展示最佳實(shí)踐的載體。

因?yàn)槭鞘职咽纸堂米訉?xiě)腳手架,所以本文很詳細(xì),字?jǐn)?shù)很多,大概有3萬(wàn)字左右,讀完你會(huì)學(xué)到:

  • 如何搭建一個(gè)腳手架的工程
  • 如何開(kāi)發(fā)和調(diào)試一個(gè)腳手架
  • 腳手架中如何接收和處理命令參數(shù)
  • 腳手架中如何和用戶交互
  • 腳手架中如何拷貝一個(gè)文件夾或文件
  • 腳手架中如何動(dòng)態(tài)生成一個(gè)文件
  • 腳手架中如何處理路徑問(wèn)題
  • 腳手架中如何自動(dòng)安裝模板所需依賴

本文中所有代碼已經(jīng)上傳到 GitHub[1]。

1、搭建一個(gè) monorepo 風(fēng)格的腳手架工程

1.1、如何用 Node.js 運(yùn)行 js

為了避免 Node.js 版本差異導(dǎo)致奇怪的問(wèn)題,建議安裝 16 以上版本的 Node.js 。

隨便找個(gè)地方建個(gè)一個(gè) index.js 文件,并在該文件中添加如下代碼:

console.log('Welcome to Mortal World');

在該文件的地址欄上輸出 cmd 打開(kāi)命令行窗口,輸入 node index.js 命令,回車運(yùn)行該命令。

image.png

可以在命令行窗口中看到打印了 Welcome to Mortals World

1.2、聲明自己的命令

如果你熟悉 Vue , 肯定對(duì) vue-cli 這個(gè)腳手架有一定了解,比如運(yùn)行 vue create myapp 命令來(lái)創(chuàng)建一個(gè) Vue 工程。

如果我們沒(méi)有運(yùn)行 npm install \-g vue-cli 安裝 vue-cli 腳手架,在命令行窗口中直接運(yùn)行 vue create myapp,會(huì)報(bào)錯(cuò),報(bào)錯(cuò)如下圖所示:

image.png

可見(jiàn) vue 不是系統(tǒng)命令, vue 只是 vue-cli 腳手架聲明的一個(gè)命令。

那怎么給腳手架聲明一個(gè)命令,其實(shí)非常簡(jiǎn)單,跟我來(lái)操作。

隨便找個(gè)地方創(chuàng)建 mortal-cli 的文件夾,進(jìn)入該文件夾后,在其地址欄上輸出 cmd 打開(kāi)命令行窗口,

image.png

運(yùn)行 npm init 命令來(lái)初始化一個(gè)腳手架工程,運(yùn)行成功后,會(huì)在該文件夾中生成一個(gè) pakeage.json 文件。

我們?cè)?pakeage.json 中添加 bin 字段,來(lái)聲明一個(gè)命令,添加后的代碼如下所示:

{
 "name""mortal-cli",
 "version""1.0.0",
 "description""",
 "main""index.js",
 "scripts": {
 "test""echo \"Error: no test specified\" && exit 1"
 },
 "author""",
 "license""ISC",
 "bin":{
 "mortal""./bin/index.js"
 }
}

這樣我們就聲明了一個(gè) mortal 的命令,另外 ./bin/index.js 是運(yùn)行 mortal 命令后會(huì)運(yùn)行的 js 文件的相對(duì)路徑。

接著我們?cè)?mortal-cli 的文件夾中創(chuàng)建一個(gè) bin 文件夾,在 bin 文件夾中創(chuàng)建一個(gè) index.js 文件,并在該文件中添加如下代碼:

js

復(fù)制代碼

#!/usr/bin/env node console.log('Welcome to Mortal World');

注意在文件頭部添加 #!/usr/bin/env node,否則運(yùn)行后會(huì)報(bào)錯(cuò)!!!

這樣就完成了一個(gè)最基礎(chǔ)的腳手架工程,接下來(lái)在命令行窗口輸入 mortal 命令,運(yùn)行該命令。

image.png

會(huì)發(fā)現(xiàn),還是報(bào)錯(cuò),提示 mortal 命令不是系統(tǒng)命令,當(dāng)然不是聲明命令的方法錯(cuò)誤。

假如你把這個(gè)腳手架發(fā)布到 npm 上后。由于 mortal-cli/pakeage.jsonname 的值為 mortal-cli,所以我們運(yùn)行 npm install \-g mortal-cli 將腳手架安裝到本地后,再運(yùn)行 mortal 命令,就會(huì)發(fā)現(xiàn)運(yùn)行成功。

在實(shí)際開(kāi)發(fā)腳手架過(guò)程中不可能這么做,所以我們還要實(shí)現(xiàn)本地調(diào)試腳手架的能力,實(shí)現(xiàn)起來(lái)也非常簡(jiǎn)單,一個(gè)命令搞定。

這個(gè)命令就是 npm link,哈哈想不到吧,在這里就不講述其原理,如果你們需要的話,可以留言,我開(kāi)個(gè)單章講述一下。

輸入 npm link 命令運(yùn)行后,再輸入 mortal 命令,回車運(yùn)行,看到的結(jié)果如下圖所示。

image.png

到此,我們成功的聲明了一個(gè) mortal 命令。

1.3、npm link 的弊端

使用 npm link 來(lái)實(shí)現(xiàn)本地調(diào)試有一個(gè)弊端。比如在本地有多個(gè)版本的腳手架倉(cāng)庫(kù),在倉(cāng)庫(kù)A中修改代碼后,運(yùn)行 mortal 命令后,發(fā)現(xiàn)更改的代碼不生效。這是因?yàn)橐呀?jīng)在倉(cāng)庫(kù)B的腳手架工程中運(yùn)行 npm link,導(dǎo)致我們?cè)谶\(yùn)行 mortal 命令后是執(zhí)行倉(cāng)庫(kù)B中的代碼,在倉(cāng)庫(kù)A中修改代碼能生效才怪。要先在倉(cāng)庫(kù)B的腳手架工程中運(yùn)行 npm unlink 后,然后在倉(cāng)庫(kù)A中的腳手架工程中運(yùn)行 npm link 后,修改倉(cāng)庫(kù)A中的代碼才能生效。

為了解決這個(gè)弊端,我們使用 pnpm 來(lái)搭建 monorepo 風(fēng)格的腳手架工程。

monorepo 風(fēng)格的工程中可以含有多個(gè)子工程,且每個(gè)子工程都可以獨(dú)立編譯打包后將產(chǎn)物發(fā)成 npm 包,故又稱 monorepo 為多包工程。

由于腳手架發(fā)布到 npm 上的包名為 mortal-cli ,修改調(diào)試子工程的 package.json 文件中的代碼,代碼修改部分如下所示:

{
 "scripts": {
 "mortal""mortal --help"
 },
 "dependencies": {
 "mortal-cli""workspace:*"
 }
}

注意,在 dependencies 字段中聲明 mortal-cli 依賴包的版本,要用workspace:* 來(lái)定義,而不是具體版本號(hào)來(lái)定義。

在 pnpm 中使用 workspace: 協(xié)議定義某個(gè)依賴包版本號(hào)時(shí),pnpm 將只解析存在工作空間內(nèi)的依賴包,不會(huì)去下載解析 npm 上的依賴包。

mortal-cli 依賴包引入,執(zhí)行 pnpm i 安裝依賴,其效果就跟執(zhí)行 npm install \-g mortal-cli一樣,只不過(guò)不是全局安裝而已,只在調(diào)試子工程內(nèi)安裝 mortal-cli 腳手架。然后調(diào)試子工程就直接引用腳手架子工程本地編譯打包后的產(chǎn)物,而不是發(fā)布到 npm 上的產(chǎn)物,徹底做到本地調(diào)試。

另外腳手架子工程和調(diào)試子工程是在同一個(gè)工程中,這樣就做一對(duì)一的調(diào)試,從而解決了使用 npm link 來(lái)實(shí)現(xiàn)本地調(diào)試的弊端。

同時(shí)在 scripts 定義了腳本命令,在調(diào)試工程中執(zhí)行 pnpm mortal 既是執(zhí)行了 mortal 命令,不用腳手架工程中執(zhí)行 npm link 就可以運(yùn)行 mortal 命令。

1.4、monorepo 風(fēng)格的腳手架工程

下面開(kāi)始使用 pnpm 搭建 monorepo 風(fēng)格的腳手架工程,首先在命令行窗口中輸入以下代碼,執(zhí)行安裝 pnpm 。

iwr https://get.pnpm.io/install.ps1 -useb | iex

然后重新找個(gè)地方創(chuàng)建 mortal 文件夾,進(jìn)入該文件夾后,在其地址欄上輸出 cmd 打開(kāi)命令行窗口。

輸入 pnpm init 初始化工程,pnpm 是使用 workspace (工作空間) 來(lái)搭建一個(gè) monorepo 風(fēng)格的工程。

所以我們要 mortal 文件夾中創(chuàng)建 pnpm-workspace.yaml 工作空間配置文件,并在該文件中添加如下配置代碼。

packages: 
 - 'packages/*' 
 - 'examples/*'

配置后,聲明了 packagesexamples 文件夾中子工程是同屬一個(gè)工作空間的,工作空間中的子工程編譯打包的產(chǎn)物都可以被其它子工程引用。

packages 文件夾中新建 mortal-cli 文件夾,在其地址欄上輸出 cmd 打開(kāi)命令行窗口。

輸入 pnpm init 命令,執(zhí)行來(lái)初始化一個(gè)工程,執(zhí)行成功后,會(huì)在該文件夾中生成一個(gè) pakeage.json 文件。

我們?cè)?pakeage.json 中添加 bin 字段,來(lái)聲明 mortal 命令,添加后的代碼如下所示:

{
 "name""mortal-cli",
 "version""1.0.0",
 "description""",
 "main""index.js",
 "scripts": {
 "test""echo \"Error: no test specified\" && exit 1"
 },
 "author""",
 "license""ISC",
 "bin":{
 "mortal""./bin/index.js"
 }
}

packages/mortal-cli 文件夾中新建 bin 文件夾,在 bin 文件夾中新建 index.js 文件,并在該文件中添加如下代碼:

#!/usr/bin/env node
console.log('Welcome to Mortal World');

examples 文件夾中新建 app 文件夾,在其地址欄上輸出 cmd 打開(kāi)命令行窗口, 運(yùn)行 pnpm init 命令來(lái)初始化一個(gè)工程,運(yùn)行成功后,會(huì)在該文件夾中生成一個(gè) pakeage.json 文件。

我們?cè)?pakeage.json 中添加 dependencies 字段,來(lái)添加 mortal-cli 依賴。再給 scripts 增加一條自定義腳本命令。添加后的代碼如下所示:

{
 "name""app",
 "version""1.0.0",
 "description""",
 "main""index.js",
 "scripts": {
 "mortal""mortal"
 },
 "author""",
 "license""ISC",
 "dependencies": {
 "mortal-cli""workspace:*"
 }
}

然后在最外層根目錄下運(yùn)行 pnpm i 命令,安裝依賴。安裝成功后,在 app 文件夾目錄下運(yùn)行 pnpm mortal,會(huì)發(fā)現(xiàn)命令行窗口打印出 Welcome to Mortal World,說(shuō)明你的 monorepo 風(fēng)格的腳手架工程的搭建成功了。

image.png

此時(shí)整個(gè)工程的目錄結(jié)構(gòu)如下所示

  |-- mortal
      |-- package.json
      |-- pnpm-lock.yaml
      |-- pnpm-workspace.yaml
      |-- examples
      |   |-- app
      |       |-- package.json
      |-- packages
          |-- mortal-cli
              |-- package.json
              |-- bin
                  |-- index.js


1.5、腳手架必備的模塊

一個(gè)最簡(jiǎn)單的腳手架包含以下幾個(gè)模塊。

  • 命令參數(shù)模塊
  • 用戶交互模塊
  • 文件拷貝模塊
  • 動(dòng)態(tài)文件生成模塊
  • 自動(dòng)安裝依賴模塊

下面我們來(lái)一一將他們實(shí)現(xiàn)。

2、命令參數(shù)模塊

2.1、獲取命令參數(shù)

Node.js 中的 process 模塊提供了當(dāng)前 Node.js 進(jìn)程相關(guān)的全局環(huán)境信息,比如命令參數(shù)、環(huán)境變量、命令運(yùn)行路徑等等。

const process = require('process');
// 獲取命令參數(shù)
console.log(process.argv);

腳手架提供的 mortal 命令后面還可以設(shè)置參數(shù),標(biāo)準(zhǔn)的腳手架命令參數(shù)需要支持兩種格式,比如:

mortal --name=orderPage
mortal --name orderPage

如果通過(guò) process.argv 來(lái)獲取,要額外處理兩種不同的命令參數(shù)格式,不方便。

這里推薦 yargs 開(kāi)源庫(kù)來(lái)解析命令參數(shù)。運(yùn)行以下命令安裝 yargs

pnpm add yargs --F mortal-cli

pnpm addpnpm 中安裝依賴包的命令, --F mortal-cli,是指定依賴安裝到 mortal-cli 子工程中。

這里要注意,mortal-cli 是取 mortal-cli 子工程中 package.jsonname 字段的值,而不是 mortal-cli 子工程文件夾的名稱。

yargs 的使用非常簡(jiǎn)單,其提供的 argv 屬性是對(duì)兩個(gè)格式的命令參數(shù)的處理結(jié)果。

bin/index.js 添加如下代碼:

#!/usr/bin/env node
const yargs = require('yargs');
console.log('name', yargs.argv.name);

注意,以上代碼是在 Node.js 環(huán)境中運(yùn)行,Node.js 的模塊是遵循 CommonJS 規(guī)范的,如果要依賴一個(gè)模塊,要使用 Node.js 內(nèi)置 require 系統(tǒng)函數(shù)引用模塊使用。

app 文件夾目錄下運(yùn)行 pnpm mortal \-- \--name=orderPage ,

注意,在 pnpm mortal 后面需要加上兩個(gè)連字符(--),這是為了告訴 pnpm 后面的參數(shù)是傳遞給命令mortal 本身的,而不是傳遞給 pnpm 的。

結(jié)果如下圖所示:

image.png

可以通過(guò) yargs.argv.name 獲取命令參數(shù) name 的值。

2.2、設(shè)置子命令

假如腳手架要對(duì)外提供多個(gè)功能,不能將所有的功能都集中在 mortal 命令中實(shí)現(xiàn)。

可以通過(guò) yargs 提供的 command 方法來(lái)設(shè)置一些子命令,讓每個(gè)子命令對(duì)應(yīng)各自功能,各司其職。

yargs.command 的用法是 **yargs.command(cmd, desc, builder, handler)**。

  • cmd:字符串,子命令名稱,也可以傳遞數(shù)組,如 ['create', 'c'],表示子命令叫 create,其別名是 c
  • desc:字符串,子命令描述信息;
  • builder:一個(gè)返回?cái)?shù)組的函數(shù),子命令參數(shù)信息配置,比如可以設(shè)置參數(shù):
    • alias:別名;
    • demand:是否必填;
    • default:默認(rèn)值;
    • describe:描述信息;
    • type:參數(shù)類型,string | boolean | number。
  • handler: 函數(shù),可以在這個(gè)函數(shù)中專門(mén)處理該子命令參數(shù)。

下面我們來(lái)設(shè)置一個(gè)用來(lái)生成一個(gè)模板的子命令,把這個(gè)子命令命名為create。

修改在 bin/index.js 文件中的代碼,如下所示:

#!/usr/bin/env node
const yargs = require('yargs');
yargs.command(
 ['create''c'],
 '新建一個(gè)模板',
 function (yargs{
   return yargs.option('name', {
     alias'n',
     demandtrue,
     describe'模板名稱',
     type'string'
   })
 },
 function (argv{
   console.log('argv', argv);
 }
).argv;

app 文件夾目錄下分別運(yùn)行 pnpm mortal create \-- \--name=orderPagepnpm mortal c \-- \--name=orderPage 命令,執(zhí)行結(jié)果如下圖所示:

image.png

在上面我們配置了子命令 create 的參數(shù) name 的一些參數(shù)信息。那這些要怎么展示給用戶看呢?其實(shí)只要我們輸入子命令的參數(shù)有錯(cuò)誤,就會(huì)在命令行窗口中顯示這些參數(shù)信息。

app 文件夾目錄下運(yùn)行 pnpm mortal c \-- \--abc 命令,執(zhí)行結(jié)果如下圖所示:

image.png

到此為止,我們最簡(jiǎn)單地實(shí)現(xiàn)了腳手架和用戶之間的交互能力,但是如果自定義參數(shù)過(guò)多,那么命令行參數(shù)的交互方法對(duì)于用戶來(lái)說(shuō)是非常不友好的。所以我們還要實(shí)現(xiàn)一個(gè)用戶交互模塊,如何實(shí)現(xiàn)請(qǐng)看下一小節(jié)。

3、用戶交互模塊

我認(rèn)為比較好的用戶交互方式是訊問(wèn)式的交互,比如我們?cè)谶\(yùn)行 npm init,通過(guò)詢問(wèn)式的交互完成 package.json 文件內(nèi)容的填充。

image.png

這里推薦使用 inquirer 開(kāi)源庫(kù)來(lái)實(shí)現(xiàn)詢問(wèn)式的交互,運(yùn)行以下命令安裝 inquirer

js

復(fù)制代碼

pnpm add [email protected] --F mortal-cli

為了使用 require 引入 inquirer ,要使用 8.2.5 版本的 inquirer

這里我們主要使用了 inquirer 開(kāi)源庫(kù)的三個(gè)方面的能力:

  • 詢問(wèn)用戶問(wèn)題
  • 獲取并解析用戶的輸入
  • 檢測(cè)用戶的答案是否合法

主要通過(guò) inquirer.prompt() 來(lái)實(shí)現(xiàn)。prompt 函數(shù)接收一個(gè)數(shù)組,數(shù)組的每一項(xiàng)都是一個(gè)詢問(wèn)項(xiàng),詢問(wèn)項(xiàng)有很多配置參數(shù),下面是常用的配置項(xiàng)。

  • type:提問(wèn)的類型,常用的有
    • 輸入框:input;
    • 確認(rèn):confirm
    • 單選組:list;
    • 多選組:checkbox;
  • name:存儲(chǔ)當(dāng)前問(wèn)題答案的變量;
  • message:?jiǎn)栴}的描述;
  • default:默認(rèn)值;
  • choices:列表選項(xiàng),在某些type下可用;
  • validate:對(duì)用戶的答案進(jìn)行校驗(yàn);
  • filter:對(duì)用戶的答案進(jìn)行過(guò)濾處理,返回處理后的值。

比如我們創(chuàng)建一個(gè)模板文件,大概會(huì)詢問(wèn)用戶:模板文件名稱、模板類型、使用什么框架開(kāi)發(fā)、使用框架對(duì)應(yīng)的哪個(gè)組件庫(kù)開(kāi)發(fā)等等。下面我們來(lái)實(shí)現(xiàn)這個(gè)功能。

bin 文件夾中新建 inquirer.js 文件夾,在里面添加如下代碼:

const inquirer = require('inquirer');

function inquirerPrompt(argv{
  const { name } = argv;
  return new Promise((resolve, reject) => {
    inquirer.prompt([
      {
        type'input',
        name'name',
        message'模板名稱',
        default: name,
        validatefunction (val{
          if (!/^[a-zA-Z]+$/.test(val)) {
            return "模板名稱只能含有英文";
          }
          if (!/^[A-Z]/.test(val)) {
            return "模板名稱首字母必須大寫(xiě)"
          }
          return true;
        },
      },
      {
        type'list',
        name'type',
        message'模板類型',
        choices: ['表單''動(dòng)態(tài)表單''嵌套表單'],
        filterfunction (value{
          return {
            '表單'"form",
            '動(dòng)態(tài)表單'"dynamicForm",
            '嵌套表單'"nestedForm",
          }[value];
        },
      },
      {
        type'list',
        message'使用什么框架開(kāi)發(fā)',
        choices: ['react''vue'],
        name'frame',
      }
    ]).then(answers => {
      const { frame } = answers;
      if (frame === 'react') {
        inquirer.prompt([
          {
            type'list',
            message'使用什么UI組件庫(kù)開(kāi)發(fā)',
            choices: [
              'Ant Design',
            ],
            name'library',
          }
        ]).then(answers1 => {
          resolve({
            ...answers,
            ...answers1,
          })
        }).catch(error => {
          reject(error)
        })
      }

      if (frame === 'vue') {
        inquirer.prompt([
          {
            type'list',
            message'使用什么UI組件庫(kù)開(kāi)發(fā)',
            choices: [ 'Element'],
            name'library',
          }
        ]).then(answers2 => {
          resolve({
            ...answers,
            ...answers2,
          })
        }).catch(error => {
          reject(error)
        })
      }
    }).catch(error => {
      reject(error)
    })
  })

}

exports.inquirerPrompt = inquirerPrompt;

其中 inquirer.prompt() 返回的是一個(gè) Promise,我們可以用 then 獲取上個(gè)詢問(wèn)的答案,根據(jù)答案再發(fā)起對(duì)應(yīng)的內(nèi)容。

bin/index.js 中引入 inquirerPrompt

#!/usr/bin/env node

const yargs = require('yargs');
const { inquirerPrompt } = require("./inquirer");

yargs.command(
  ['create''c'],
  '新建一個(gè)模板',
  function (yargs{
    return yargs.option('name', {
      alias'n',
      demandtrue,
      describe'模板名稱',
      type'string'
    })
  },
  function (argv{
    inquirerPrompt(argv).then(answers =>{
      console.log(answers)
    })
  }
).argv;

app 文件夾目錄下運(yùn)行 pnpm mortal c \-- \--n Input 命令,執(zhí)行結(jié)果如下圖所示:

image.png
image.png

可以很清楚地看到“在使用什么框架開(kāi)發(fā)”的詢問(wèn)中回答不同,下一個(gè)“使用什么UI組件庫(kù)的開(kāi)發(fā)”的詢問(wèn)可選項(xiàng)不一樣。

回答完成后,可以在下圖中清楚地看到答案格式

image.png

4、文件夾拷貝模塊

要生成一個(gè)模板文件,最簡(jiǎn)單的做法就是執(zhí)行腳手架提供的命令后,把腳手架中的模板文件,拷貝到對(duì)應(yīng)的地方。模板文件可以是單個(gè)文件,也可以是一個(gè)文件夾。本小節(jié)先介紹一下模板文件是文件夾時(shí)候如何拷貝。

Node.js 中拷貝文件夾并不簡(jiǎn)單,需要用到遞歸,這里推薦使用開(kāi)源庫(kù)copy-dir來(lái)實(shí)現(xiàn)拷貝文件。

運(yùn)行以下命令安裝 copy-dir 。

pnpm add copy-dir --F mortal-cli

bin 文件夾中新建 copy.js 文件,在里面添加如下代碼:

const copydir = require('copy-dir');
const fs = require('fs');
function copyDir(from, to, options{
 copydir.sync(from, to, options);
}
function checkMkdirExists(path{
 return fs.existsSync(path)
};
exports.checkMkdirExists = checkMkdirExists;
exports.copyDir = copyDir;

copyDir 方法實(shí)現(xiàn)非常簡(jiǎn)單,難的是如何使用,下面創(chuàng)建一個(gè)場(chǎng)景來(lái)介紹一下如何使用。

我們?cè)?bin 文件夾中新建 template 文件夾,用來(lái)存放模板文件,比如在 template 文件夾中創(chuàng)建一個(gè) form 文件夾來(lái)存放表單模板,這里不介紹表單模板的內(nèi)容,我們隨意在 form 文件夾中創(chuàng)建一個(gè) _index.js_,在里面隨便寫(xiě)些內(nèi)容。其目錄結(jié)構(gòu)如下所示:

  |-- mortal
      |-- package.json
      |-- pnpm-lock.yaml
      |-- pnpm-workspace.yaml
      |-- examples
      |   |-- app
      |       |-- package.json
      |-- packages
          |-- mortal
              |-- package.json
              |-- bin
                  |-- template
                      |-- form
                          |-- index.js
                  |-- copy.js
                  |-- index.js
                  |-- inquirer.js

下面來(lái)實(shí)現(xiàn)把 packages/mortal/bin/template/form 這個(gè)文件夾拷貝到 examples/app/src/pages/OrderPage 中 。

bin/index.js 修改代碼,修改后的代碼如下所示:

#!/usr/bin/env node

const yargs = require('yargs');
const path = require('path');
const { inquirerPrompt } = require("./inquirer");
const { copyDir, checkMkdirExists } = require("./copy");

yargs.command(
  ['create''c'],
  '新建一個(gè)模板',
  function (yargs{
    return yargs.option('name', {
      alias'n',
      demandtrue,
      describe'模板名稱',
      type'string'
    })
  },
  function (argv{
    inquirerPrompt(argv).then(answers => {
      const { name, type } = answers;
      const isMkdirExists = checkMkdirExists(
        path.resolve(process.cwd(),`./src/pages/${name}`)
      );
      if (isMkdirExists) {
        console.log(`${name}文件夾已經(jīng)存在`)
      } else {
        copyDir(
          path.resolve(__dirname, `./template/${type}`),
          path.resolve(process.cwd(), `./src/pages/${name}`)
        )
      }
    })
  }
).argv;

使用拷貝文件方法 copyDir 的難點(diǎn)是參數(shù) fromto 的賦值。其中 from 表示要拷貝文件的路徑,to 表示要把文件拷貝到那里的路徑。這里的路徑最好使用絕對(duì)路徑,因?yàn)樵?Node.js 中使用相對(duì)路徑會(huì)出現(xiàn)一系列奇奇怪怪的問(wèn)題。

4.1、腳手架中的路徑處理

我們可以用 Node.js 中的 path 模塊提供的 path.resolve( [from…], to ) 方法將路徑轉(zhuǎn)成絕對(duì)路徑,就是將參數(shù) to 拼接成一個(gè)絕對(duì)路徑,[from … ] 為選填項(xiàng),可以設(shè)置多個(gè)路徑,如 path.resolve('./aaa', './bbb', './ccc') ,使用時(shí)要注意path.resolve 的路徑拼接規(guī)則:

  • 從后向前拼接路徑;
  • to/ 開(kāi)頭,不會(huì)拼接到前面的路徑;
  • to../ 開(kāi)頭,拼接前面的路徑,且不含最后一節(jié)路徑;
  • to./ 開(kāi)頭或者沒(méi)有符號(hào),則拼接前面路徑。

從以上拼接規(guī)則來(lái)看,使用 path.resolve 時(shí),要特別注意參數(shù) to 的設(shè)置。

下面來(lái)介紹一下,使用 copyDir 方法時(shí),參數(shù)如何設(shè)置:

  • copyDir 的參數(shù) from 設(shè)置為 path.resolve(__dirname, `./template/${type}`)

其中 __dirname 是用來(lái)動(dòng)態(tài)獲取當(dāng)前文件模塊所屬目錄的絕對(duì)路徑。比如在 bin/index.js 文件中使用 __dirname ,__dirname 表示就是 bin/index.js 文件所屬目錄的絕對(duì)路徑 D:\mortal\packages\mortal-cli\bin

因?yàn)槟0逦募娣旁?bin/template 文件夾中 ,copyDir 是在 bin/index.js 中使用,_bin/template_ 文件夾相對(duì) bin/index.js 文件的路徑是 ./template,所以把 path.resolve 的參數(shù) to 設(shè)置為 ./template/${type},其中 type 是用戶所選的模板類型。

假設(shè) type 的模板類型是 form,那么 path.resolve(__dirname, `./template/form`) 得到的絕對(duì)路徑是 D:\mortal\packages\mortal-cli\bin\template\form。

  • copyDir 的參數(shù) to 設(shè)置為 path.resolve(process.cwd(), `${name}`)

其中 process.cwd() 當(dāng)前 Node.js 進(jìn)程執(zhí)行時(shí)的文件所屬目錄的絕對(duì)路徑。比如在 bin 文件夾目錄下運(yùn)行 node index.js 時(shí),process.cwd() 得到的是 D:\mortal\packages\mortal-cli\bin。

運(yùn)行 node index.js 相當(dāng)運(yùn)行 mortal 命令。而在現(xiàn)代前端工程中都是在 package.json 文件中scripts 定義了腳本命令,如下所示:

{
  "name""app",
  "version""1.0.0",
  "description""",
  "main""index.js",
  "scripts": {
    "mortal""mortal"
  },
  "author""",
  "license""ISC",
  "dependencies": {
    "mortal-cli""workspace:*"
  }
}

運(yùn)行 pnpm mortal 就相當(dāng)運(yùn)行 mortal 命令,那么執(zhí)行 pnpm mortal 時(shí),當(dāng)前 Node.js 進(jìn)程執(zhí)行時(shí)的文件是 package.json 文件。那么 process.cwd() 得到的是 D:\mortal\examples\app。

因?yàn)橐?packages/mortal/bin/template/form 這個(gè)文件夾拷貝到 examples/app/src/pages/OrderPage 中,且 process.cwd() 的值是D:\mortal\examples\app,_src/pages_ 文件夾相對(duì) examples/app 的路徑是 ./src/pages ,所以把 path.resolve 的參數(shù) to 設(shè)置為 ./src/pages/${name},其中 name 是用戶所輸入的模板名稱。

4.2、目錄守衛(wèi)

app 文件夾目錄下運(yùn)行 pnpm mortal create \-- \--name=OrderPage,看能不能成功得把 packages/mortal/bin/template/form 這個(gè)文件夾拷貝到 examples/app/src/pages/OrderPage 中。

image.png

報(bào)錯(cuò)了,提示 examples/app/src/pages 文件夾不存在。為了防止這種報(bào)錯(cuò)出現(xiàn),我們要實(shí)現(xiàn)一個(gè)目錄守護(hù)的方法 mkdirGuard ,比如 examples/app/src/pages 文件夾不存在,就創(chuàng)建一個(gè) examples/app/src/pages 文件夾。

bin/copy.js 文件中,修改代碼,如下所示:

const copydir = require('copy-dir');
const fs = require('fs');
const path = require('path');

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);
    }
  }
}

function copyDir(form, to, options) {
  mkdirGuard(to);
  copydir.sync(form, to, options);
}

function checkMkdirExists(path) {
  return fs.existsSync(path)
};

exports.checkMkdirExists = checkMkdirExists;
exports.mkdirGuard = mkdirGuard;
exports.copyDir = copyDir;

fs.mkdirSync 的語(yǔ)法格式:fs.mkdirSync(path[, options]),創(chuàng)建文件夾目錄。

  • path:文件夾目錄路徑;
  • optionsrecursive 表示是否要?jiǎng)?chuàng)建父目錄,true 要。

fs.existsSync 的語(yǔ)法格式:fs.existsSync(pach),檢測(cè)目錄是否存在,如果目錄存在返回 true ,如果目錄不存在返回false

  • path:文件夾目錄路徑。

path.dirname 的語(yǔ)法格式:path.dirname(path),用于獲取給定路徑的目錄名。

  • path:文件路徑。

mkdirGuard 方法內(nèi)部,當(dāng)要?jiǎng)?chuàng)建的目錄 target 父級(jí)目錄不存在時(shí),調(diào)用fs.mkdirSync(target),會(huì)報(bào)錯(cuò)走 catch 部分邏輯,在其中遞歸創(chuàng)建父級(jí)目錄,使用 fs.existsSync(dir) 來(lái)判斷父級(jí)目錄是否存在,來(lái)終止遞歸。這里要特別注意 fs.mkdirSync(dir) 創(chuàng)建父級(jí)目錄要在 mkdirp(dirname) 之前調(diào)用,才能形成一個(gè)正確的創(chuàng)建順序,否則創(chuàng)建父級(jí)目錄過(guò)程會(huì)因父級(jí)目錄的父級(jí)目錄不存在報(bào)錯(cuò)。

我們?cè)俅卧?app 文件夾目錄下運(yùn)行 pnpm mortal create \-- \--name=OrderPage,看這次能不能成功得把 packages/mortal/bin/template/form 這個(gè)文件夾拷貝到 examples/app/src/pages/OrderPage 中。

成功添加,添加結(jié)果如下所示:

image.png

然后再運(yùn)行 pnpm mortal create \-- \--name=OrderPage 命令,會(huì)發(fā)現(xiàn)控制臺(tái)打印出模板已經(jīng)存在在提示。

image.png

這是為了防止用戶修改后的模板文件,運(yùn)行命令后被重新覆蓋到初始狀態(tài)。所以我們引入一個(gè)校驗(yàn)?zāi)0逦募欠翊嬖诘?checkMkdirExists 方法,內(nèi)部采用 fs.existsSync 來(lái)實(shí)現(xiàn)。

5、文件拷貝模塊

文件拷貝分三步來(lái)實(shí)現(xiàn),使用 fs.readFileSync 讀取被拷貝的文件內(nèi)容,然后創(chuàng)建一個(gè)文件,再使用 fs.writeFileSync 寫(xiě)入文件內(nèi)容。

bin/copy.js 文件,在里面添加如下代碼:

function copyFile(from, to{
 const buffer = fs.readFileSync(from);
 const parentPath = path.dirname(to);
 mkdirGuard(parentPath)
 fs.writeFileSync(to, buffer);
}
exports.copyFile = copyFile;

接下來(lái)我們使用 copyFile 方法,在 bin/index.js 修改代碼,修改后的代碼如下所示:

#!/usr/bin/env node

const yargs = require('yargs');
const path = require('path');
const { inquirerPrompt } = require("./inquirer");
const { copyDir } = require("./copy");

yargs.command(
  ['create''c'],
  '新建一個(gè)模板',
  function (yargs{
    return yargs.option('name', {
      alias'n',
      demandtrue,
      describe'模板名稱',
      type'string'
    })
  },
  function (argv{
    inquirerPrompt(argv).then(answers => {
      const { name, type } = answers;
      const isMkdirExists = checkMkdirExists(
        path.resolve(process.cwd(),`./src/pages/${name}/index.js`)
      );
      if (isMkdirExists) {
        console.log(`${name}/index.js文件已經(jīng)存在`)
      } else {
        copyFile(
          path.resolve(__dirname, `./template/${type}/index.js`),
          path.resolve(process.cwd(), `./src/pages/${name}/index.js`),
          {
            name,
          }
        )
      }
    })
  }
).argv;

copyFilecopyDir 使用的區(qū)別在參數(shù),copyFile 要求參數(shù) from 和參數(shù) to 都精確到文件路徑。

app 文件夾目錄下運(yùn)行 pnpm mortal create \-- \--name=OrderPage,執(zhí)行結(jié)果如下圖所示:

image.png

6、動(dòng)態(tài)文件生成模塊

假設(shè)腳手架中提供的模板文件中某些信息需要根據(jù)用戶輸入的命令參數(shù)來(lái)動(dòng)態(tài)生成對(duì)應(yīng)的模板文件。

比如下面模板文件中 App 要?jiǎng)討B(tài)替換成用戶輸入的命令參數(shù) name 的值,該如何實(shí)現(xiàn)呢?

import React from 'react';
const App = () => {
 return (
 <div></div>
 );
};
export default App;

這里推薦使用開(kāi)源庫(kù)mustache來(lái)實(shí)現(xiàn),運(yùn)行以下命令安裝 copy-dir 。

pnpm add mustache --F mortal-cli

我們?cè)?packages/mortal-cli/bin/template/form 文件夾中創(chuàng)建一個(gè) index.tpl 文件,內(nèi)容如下:

import React from 'react';
const {{name}} = () => {
 return (
 <div></div>
 );
};
export default {{name}};

先寫(xiě)一個(gè) readTemplate 方法來(lái)讀取這個(gè) index.tpl 動(dòng)態(tài)模板文件內(nèi)容。在 bin/copy.js 文件,在里面添加如下代碼:

const Mustache = require('mustache');
function readTemplate(path, data = {}) {
 const str = fs.readFileSync(path, { encoding: 'utf8' })
 return Mustache.render(str, data);
}
exports.readTemplate = readTemplate;

readTemplate 方法接收兩個(gè)參數(shù),path 動(dòng)態(tài)模板文件的相對(duì)路徑,data 動(dòng)態(tài)模板文件的配置數(shù)據(jù)。

使用 Mustache.render(str, data) 生成模板文件內(nèi)容返回,因?yàn)?Mustache.render 的第一個(gè)參數(shù)類型是個(gè)字符串,所以在調(diào)用 fs.readFileSync 時(shí)要指定 encoding 類型為 utf8,否則 fs.readFileSync 返回 Buffer 類型數(shù)據(jù)。

在寫(xiě)一個(gè) copyTemplate 方法來(lái)拷貝模板文件到對(duì)應(yīng)的地方,跟 copyFile 方法非常相似。在 bin/copy.js 文件,在里面添加如下代碼:

function copyTemplate(from, to, data = {}) {
 if (path.extname(from) !== '.tpl') {
   return copyFile(from, to);
 }
 const parentToPath = path.dirname(to);
 mkdirGuard(parentToPath);
 fs.writeFileSync(to, readTemplate(from, data));
}

path.extname(from) 返回文件擴(kuò)展名,比如 path.extname(index.tpl) 返回 .tpl。

bin/index.js 修改代碼,修改后的代碼如下所示:

#!/usr/bin/env node

const yargs = require('yargs');
const path = require('path');
const { inquirerPrompt } = require("./inquirer");
const { copyTemplate } = require("./copy");

yargs.command(
  ['create''c'],
  '新建一個(gè)模板',
  function (yargs{
    return yargs.option('name', {
      alias'n',
      demandtrue,
      describe'模板名稱',
      type'string'
    })
  },
  function (argv{
    inquirerPrompt(argv).then(answers => {
      const { name, type } = answers;
      const isMkdirExists = checkMkdirExists(
        path.resolve(process.cwd(),`./src/pages/${name}/index.js`)
      );
      if (isMkdirExists) {
        console.log(`${name}/index.js文件已經(jīng)存在`)
      } else {
        copyTemplate(
          path.resolve(__dirname, `./template/${type}/index.tpl`),
          path.resolve(process.cwd(), `./src/pages/${name}/index.js`),
          {
            name,
          }
        )
      }
    })
  }
).argv;

app 文件夾目錄下運(yùn)行 pnpm mortal create \-- \--name=OrderPage,執(zhí)行結(jié)果如下圖所示:

image.png

6.1、mustache 簡(jiǎn)介

以上的案例是 mustache 最簡(jiǎn)單的使用,下面來(lái)額外介紹一些常用的使用場(chǎng)景。

首先來(lái)熟悉一下 mustache 的語(yǔ)法,下面來(lái)介紹一些場(chǎng)景來(lái)使用這些語(yǔ)法

  • {{key}}
  • {{#key}} {{/key}}
  • {{^key}} {{/key}}
  • {{.}}
  • {{&key}}

6.1.1、簡(jiǎn)單綁定

使用 {{key}} 語(yǔ)法,key 要和 Mustache.render 方法中的第二個(gè)參數(shù)(一個(gè)對(duì)象)的屬性名一致。

例如:

Mustache.render('<span>{{name}}</span>',{name:'張三'})

輸出:

<span>張三</span>

6.1.2、綁定子屬性

例如:

Mustache.render('<span>{{ifno.name}}</span>', { ifno: { name: '張三' } })

輸出:

<span>張三</span>

6.1.3、循環(huán)渲染

如果 key 屬性值是一個(gè)數(shù)組,則可以使用 {{#key}} {{/key}} 語(yǔ)法來(lái)循環(huán)展示。其中 {{#}} 標(biāo)記表示從該標(biāo)記以后的內(nèi)容全部都要循環(huán)展示,{{/}}標(biāo)記表示循環(huán)結(jié)束。

例如:

Mustache.render(
 '<span>{{#list}}{{name}}{{/list}}</span>',
 {
   list: [
     { name: '張三' },
     { name: '李四' },
     { name: '王五' },
   ]
 }
)

輸出:

<span>張三李四王五</span>

如果 list 的值是 ['張三','李四','王五'],要把 {{name}} 替換成 {{.}} 才可以渲染。

Mustache.render(
 '<span>{{#list}}{{.}}{{/list}}</span>',
 {
   list: ['張三','李四','王五']
 }
)

6.1.4、循環(huán)中二次處理數(shù)據(jù)

Mustache.render 方法中的第二個(gè)參數(shù)是個(gè)對(duì)象,其屬性值可以是一個(gè)函數(shù),渲染時(shí)候會(huì)執(zhí)行函數(shù)輸出返回值,函數(shù)中可以用 this 獲取第二個(gè)參數(shù)的上下文。

例如:

Mustache.render(
 '<span>{{#list}}{{info}}{{/list}}</span>',
 {
   list: [
     { name: '張三' },
     { name: '李四' },
     { name: '王五' },
   ],
   info() {
     return this.name + ',';
   }
 }
)

輸出:

<span>張三,李四,王五,</span>

6.1.5、條件渲染

使用 {{#key}} {{/key}} 語(yǔ)法 和 {{^key}} {{/key}} 語(yǔ)法來(lái)實(shí)現(xiàn)條件渲染,當(dāng) keyfalse、0、[]、{}、null,既是 key == false 為真,{{#key}} {{/key}} 包裹的內(nèi)容不渲染,{{^key}} {{/key}} 包裹的內(nèi)容渲染

例如:

Mustache.render(
 '<span>{{#show}}顯示{{/show}}{{^show}}隱藏{{/show}}</span>',
 {
 show: false
 }
)

輸出:

<span>隱藏</span>

6.1.6、不轉(zhuǎn)義 HTML 標(biāo)簽

使用 {{&key}} 語(yǔ)法來(lái)實(shí)現(xiàn)。

例如:

Mustache.render(
 '<span>{{&key}}</span>',
 {
 key: '<span>標(biāo)題</span>'
 }
)

輸出:

<span><span>標(biāo)題</span></span>

7、自動(dòng)安裝依賴模塊

假設(shè)模板是這樣的:

import React from 'react';
import { Button, Form, Input } from 'antd';

const App = () => {
  const onFinish = (values) => {
    console.log('Success:', values);
  };
  return (
    <Form onFinish={onFinish} autoComplete="off">
      <Form.Item label="Username" name="username">
        <Input />
      </Form.Item>
      <Form.Item>
        <Button type="primary" htmlType="submit">提交</Button>
      </Form.Item>
    </Form>

  );
};
export default App;

可以看到模板中使用了 reactantd 這兩個(gè)第三方依賴,假如使用模板的工程中沒(méi)有安裝這兩個(gè)依賴,我們要實(shí)現(xiàn)在生成模板過(guò)程中就自動(dòng)安裝這兩個(gè)依賴。

我們使用 Nodechild_process 子進(jìn)程這個(gè)模塊來(lái)實(shí)現(xiàn)。

child_process 子進(jìn)程中的最常用的語(yǔ)法是:

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í)行成功后 errornull,執(zhí)行失敗后 error 為 Error 實(shí)例,stdout、stderr 為標(biāo)準(zhǔn)輸出、標(biāo)準(zhǔn)錯(cuò)誤,其格式默認(rèn)是字符串。

bin 文件夾中新建 inquirer.js 文件夾,在里面添加如下代碼:

const path = require('path');
const { exec } = require('child_process');

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{
    exec(
      command,
      {
        cwd: path.resolve(cmdPath),
      },
      function (error, stdout, stderr{
        console.log('error', error);
        console.log('stdout', stdout);
        console.log('stderr', stderr)
      }
    )
  })
}

exports.install = install;

install 方法中 exec 的參數(shù) commandpnpm 安裝依賴命令,安裝多個(gè)依賴時(shí)使用 && 拼接。參數(shù) cwd 是所安裝依賴工程的 package.json 文件路徑,我們可以使用 process.cwd() 獲取。已經(jīng)在上文提到過(guò),process.cwd() 是當(dāng)前Node.js 進(jìn)程執(zhí)行時(shí)的文件所屬目錄的絕對(duì)路徑。

接下來(lái)使用,在 bin/index.js 修改代碼,修改后的代碼如下所示:

#!/usr/bin/env node

const yargs = require('yargs');
const path = require('path');
const { inquirerPrompt } = require("./inquirer");
const { copyTemplate, checkMkdirExists } = require("./copy");
const { install } = require('./manager');

yargs.command(
  ['create''c'],
  '新建一個(gè)模板',
  function (yargs{
    return yargs.option('name', {
      alias'n',
      demandtrue,
      describe'模板名稱',
      type'string'
    })
  },
  function (argv{
    inquirerPrompt(argv).then(answers => {
      const { name, type } = answers;
      const isMkdirExists = checkMkdirExists(
        path.resolve(process.cwd(),`./src/pages/${name}/index.js`)
      );
      if (isMkdirExists) {
        console.log(`${name}/index.js文件已經(jīng)存在`)
      } else {
        copyTemplate(
          path.resolve(__dirname, `./template/${type}/index.tpl`),
          path.resolve(process.cwd(), `./src/pages/${name}/index.js`),
          {
            name,
          }
        )
        install(process.cwd(), answers);
      }
    })
  }
).argv;

當(dāng)執(zhí)行完 copyTemplate 方法后,就開(kāi)始執(zhí)行 install(process.cwd(), answers) 自動(dòng)安裝模板中所需的依賴。

app 文件夾目錄下運(yùn)行 pnpm mortal create \-- \--name=OrderPage,看能不能自動(dòng)安裝依賴。

等命令執(zhí)行完成后,觀察 examples\app\package.json 文件中的 dependencies 值是不是添加了 antdreact 依賴。

image.png

此外,我們?cè)趫?zhí)行命令中會(huì)發(fā)現(xiàn),如下圖所示的現(xiàn)象,光標(biāo)一直在閃爍,好像卡住了,其中是依賴在安裝。這里我們要引入一個(gè)加載動(dòng)畫(huà),來(lái)解決這個(gè)不友好的現(xiàn)象。

image.png

這里推薦使用開(kāi)源庫(kù)ora來(lái)實(shí)現(xiàn)加載動(dòng)畫(huà)。

運(yùn)行以下命令安裝 ora 。

pnpm add [email protected] --F mortal-cli

bin/inquirer.js 修改代碼,修改后的代碼如下所示:

const path = require('path');
const { exec } = require('child_process');
const ora = require("ora");

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(
      `正在安裝依賴,請(qǐng)稍等`
    );
    exec(
      command,
      {
        cwd: path.resolve(cmdPath),
      },
      function (error{
        if (error) {
          reject();
          spinner.fail(`依賴安裝失敗`);
          return;
        }
        spinner.succeed(`依賴安裝成功`);
        resolve()
      }
    )
  })
}

exports.install = install;

app 文件夾目錄下運(yùn)行 pnpm mortal create \-- \--name=OrderPage,看一下執(zhí)行效果。

image.png
image.png

8、發(fā)布和安裝

packages/mortal 文件夾目錄下運(yùn)行,運(yùn)行以下命令安裝將腳手架發(fā)布到 npm 上。

js

復(fù)制代碼

pnpm publish --F mortal-cli

發(fā)布成功后。我們?cè)谝粋€(gè)任意工程中,執(zhí)行 pnpm add mortal-cli \-D 安裝 mortal-cli 腳手架依賴成功后,在工程中執(zhí)行 pnpm mortal create \-- \--name=OrderPage 命令即可。

結(jié)語(yǔ)

上面只教大家實(shí)現(xiàn)一個(gè)最最簡(jiǎn)單的腳手架。其功能就只有一個(gè)模板文件生成。雖然簡(jiǎn)單,但是這些都是腳手架的入門(mén)功,代碼已經(jīng)上傳到 GitHub[2],大家可以下載下來(lái),自己實(shí)踐一下,光看不練永遠(yuǎn)學(xué)不會(huì)。

學(xué)會(huì)了,可以總結(jié)一些平時(shí)的業(yè)務(wù)代碼,形成最佳實(shí)踐,使用腳手架作為載體展現(xiàn)出來(lái),提升自己的職場(chǎng)競(jìng)爭(zhēng)力。

8、參考資料

[1]

https://github.com/532pyh/mortal-cli: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2F532pyh%2Fmortal-cli

[2]

https://github.com/532pyh/mortal-cli: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2F532pyh%2Fmortal-cli


關(guān)于本文

作者:紅塵煉心

https://juejin.cn/post/7260144602471776311

瀏覽 670
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)
評(píng)論
圖片
表情
推薦
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 成人黄色视频网站在线观看| 亚洲欧美性爱| 天天插夜夜操| 狠狠综合网| 波多野吉衣高清无码| 中文字幕在线高清| 精品视频久久久久久| 日韩在线中文字幕| 久久久1| 中文字幕人妻丰满熟妇| 丁香婷婷五月色成人网站| 五月婷婷网| 艹美女视频| 亲子乱婬-一级A片| 久久无码成人| 免费中文资源在线观看| 俺来也俺去也| www.91久久| 亚洲毛片视频| 青娱乐国产视频| 日韩av高清| 婷婷五月天啪啪| 欧美一级免费视频| 亚洲视频入口| 日韩91在线视频| 吴梦梦《女教师时间暂停》| 成人午夜福利| 神马午夜精品96| 99精品在线免费观看| 日本国产精品| 97精品视频在线观看| 色噜噜狠狠一区二区三区牛牛影视 | 无码免费毛片一区二区三区古代| AV第一福利大全导航| 天堂网www| 91精品国产综合久久久蜜臀图片 | 日本成人视频在线免费播放 | 超碰在线网| 久久青留社区金玉| 超碰自拍私拍二区三区区| 黄色电影免费在线观看| 一本大道东京热av无码| 亚洲精品高清无码| 久久久国产91桃色一区二区三区| 亚洲国产黄色视频| 日本老熟妇| 欧美亚洲国产视频| 国产精品国产三级囯产普通话2| 久久久久久黄| 国产亚洲无码| 婷婷丁香人妻天天爽| 亚洲中文字幕av天堂| 国产又爽又黄免费视频网站| 日韩无码精品视频| 欧美亚洲视频| 91精品国产综合久久久蜜臀酒店 | 亚洲天堂视频在线观看免费| 麻豆黄色| 丁香五月激情啪啪啪| 99久热在线精品视频| 可以免费看的黄色视频| 日本黄色免费在线观看| 国产香蕉视频在线播放| 欧美性性性| 国产日韩一区二区三免费高清| 三级片男人天堂| 国产一区二区三区视频在线| 久久艹精品视频| 三级高清无码视频| 色九月婷婷| 免费日韩AV| 欧美无遮挡| 亚洲高清视频无码| 五月天丁香成人| 4080yy午夜理论片成人| 青青草免费福利视频| 亚洲高清无码免费在线观看| 午夜av在线播放| 欧美激情性爱网站| 免费看a的网站| 在线成人视频网站大香蕉在线网站 | 爱爱帝国综合社区| gogogo视频在线观看黑人| 2015中文字幕黄色视频| 国产亚洲aⅴ| 少妇BBBBBB| 婷婷深爱五月丁香网| 日韩肏屄视频在线观看| а√在线中文网新版地址在线 | 国产日本欧美韩国久久久久| 日本一节片在线播放| 777免费观看成人电影视频| 99精品六月婷婷综合在线| 国产AV自拍-久| 99热精品免费在线观看| 国产特黄| 中文亚洲精品字幕电影| 亚洲中文免费| 一区四区视频| 国产一级二级片| 黄色小说视频| 天啪| 久久嫩草国产成人一区| 91成人无码看片在线观看网址| 色情片在线观看| 欧美精品日韩在线观看| 四川少扫搡BBBBB搡B| 久久免费视频精品| 亚洲无码高清视频在线观看| 中文字幕在线观看亚洲| 一级日韩| 亚洲天媒在线播放| 亚洲无码aa| 国产激情123区| 狠狠撸综合| 亚洲熟妇在线观看| 国产中文字幕第一页| 九九久久免费视频| 国产无码AV在线| 99人妻在线| 欧美三级大片| 亚洲成人无码AV| 色噜噜av| 99在线精品视频| 亚洲女与黑人正在播放| 国产美女操逼网站| 16一17女人毛片| 日韩亚洲在线视频| 91丨九色丨老熟女探花| 亚洲中文字幕免费在线观看 | 91伊人| 亚洲视频a| 狠狠操狠狠撸| 久久思思热| 一区二区三区国产| 国产三级国产三级国产普通话 | 91精品国产综合久久久蜜臀九色 | 亚州中文字幕| 人人爽人人爱| 91在线小视频| 久久免费精品| 午夜成人福利视频在线观看| 艹B视频| 亚洲欧美在线成人| 日本成人电影一区二区三区| 欧美日韩大片| 中文字幕2018第一页| 亚洲一区二区在线免费观看| 久久久成人网| 老女人操逼网| 无码观看视频| 另类视频在线| 高潮AV在线观看| 一卡二卡无码| 亚洲专区免费| 成人精东影业JDAV3密友| 久9久9| 国产老女人农村HD| 俺去俺来也www色官网黑人 | 亚洲综合中文字幕在线播放| 麻豆视频一区二区三区| 亚洲专区在线播放| а√最新版天堂中文在线| 亚洲国产精品视频| 天天干强奸视频在线综合| 爱爱电影无码| 一区二区三区www污污污网站| 久操视频免费在线观看| 69国产精品| 午夜福利黄色| 亚洲无码日| 亚洲精品日韩综合观看成人91| 色猫咪av| 刘玥无码| 欧美三P囗交做爰XXXⅩ| 手机在线小视频| 奇米色色色| 香蕉伊人在线| 女人18特级毛片。| 大逼影院| 亚洲vs无码秘蜜桃| 北条麻妃99精品| 日韩午夜精品| 青榴视频免费观看| 高清免费在线中文Av| 97人妻一区二区精品视频| 国产在线免费视频| 性生活毛片| 土耳其电影《爱与罚》| 日韩AV在线天堂| 久久综合操| 久久成人小电影| 成人福利在线| 国产一级影院| 好男人一区二区三区在线观看| 十八禁视频在线观看网站.www| 97在线国产| 国产成人久久| 69精品免费视频| 亚洲日韩国产成人精品久久| 中文字幕无码日韩| 亚洲高清在线播放| 人人干人人操人人爽| 亚洲第一成网站| 国产成人黄色| 黄色av免费在线观看| 亚洲综合社区在线| 欧美日韩人妻高清中文| 中国免费XXXX18| 久久午夜无码鲁丝片| 九九免费视频| 国产黄色片在线免费观看| 成人aV无码精品国产一区二区 | 久热精品视频| 亚洲中文字幕无码爆乳av| 亚洲精品18禁| 五月开心激情网| 久久加勒比| 亚洲中文字幕无码在线观看| 欧美A在线观看| 台湾无码在线| 青青草资源站| 精品免费一区二区三区四区| 麻豆三级片在线观看| 午夜日韩| 欧美肏屄网| 九九久久免费视频| 欧美成人一级A片| 国产男女性爱视频播放| 人妻操逼视频| 福利三区| 日本无码一区二区三三| 中国老少配BBwBBwBBW| 涩五月婷婷| 91做爱视频| www超碰在线| 美女网站黄色| 成人视频在线观看免费| 亚洲日本三级片| 永久免费AV无码| 91蝌蚪视频在线观看| 成人二区| 亚洲婷婷精品国产成人| 欧美黄片一区| 亚洲中文在线视频| 日本在线视频不卡| 欧美视频a| 日逼视频免费观看| 婷婷伊人久操网| 青青草成人网站| 无码人妻一区二区三区精品不付款 | 手机AV免费| 欧美色逼逼| av三级片在线观看| 天天摸天天添| 俺去也www俺去也com| 五月激情网站| 天天色天天干天天| 日本中文字幕网站| 人人看人人摸人人| 97桃色| 亚洲av自拍| 午夜天堂精品久久久久| 国产精品毛片VA一区二区三区 | 黄色激情五月| 久久大鸡吧| 亚洲国产精品18久久久久久| 人人香蕉| 黄色视频在线观看网站| 国产丰满大乳无码免费播放| 亚洲黄片免费在线观看| 无码欧美| jizz免费在线观看| 岛国av在线播放| 天天色小说| 国产一a毛一a免费观看| 国产无遮挡又黄又爽又色学生软件 | 亚洲日日干| 色诱AV| 一夲道无码专区av无码A片| 国产一级A片免费播放| 亚洲a∨| 天堂亚洲AV无码精品成人| 日韩不卡一区二区三区| 成人无码动漫A片| 午夜av在线播放| 熟女人妻在线观看| 免费看一区二区三区A片| 人人操日本| 精品成人A片久久久久久不卡三区| 蜜挑视频一区二区三区| 国产成人午夜高潮毛片| 成人无码毛片| 成人免费高清| 爱操逼网| 瘦精品无码一区二区三区四区五区六区七区八区 | 亚洲无码一级视频| 一本色道久久88加勒比| 久久久免费黄色视频| 欧美激情视频一区二区| 午夜嘿嘿| 国产成人中文字幕| 2025中文字幕| 在线观看黄色小电影| 欧美中文日韩| 亚洲a视频在线| 久久久久久久久久成人永久免费视频 | 口爆AV| 黄网国产手机在线观看| 欧美成人AA| 91精品久久人妻一区二区夜夜夜 | 日老女人逼| 三级黄片免费看| 国产视频一区二区在线观看| 色色99| 五月停亭六月,六月停亭的英语| 高清无码人妻| 国产乱叫456在线| 九九成人电影| 超碰97观看| 爱爱爱免费视频| 白丝自慰网站| 国产高清色| 六月丁香五月天| 一级a免一级a做免费线看内祥 | 简单av网| 欧美日韩在线一区| 性欧美xxxx| 国产精品成人99一区无码| 操逼电影免费| AV无码在线播放| 韩日不卡视频| 九九九免费| 日韩性AV| www男人天堂| 波多野结衣av在线观看窜天猴| 国产精品成人无码专区| 热久久亚洲中文字幕| 大香蕉在线75| 五丁香在线观看AV| 蜜臀AV在线播放| 综合色国产精品欧美在线| 国产伦精品一区二区三区视频女| 操逼的视频| 日韩欧美人妻| 骚逼视频聊天记录| 天堂a在线| 日韩视频播放在线综合| 蜜臀久久久| 四虎最新视频| 先锋影音一区二区| se99av| 大茄子熟女AV导航| 天天日天天日天天日| 久久免费国产视频| 中字无码av| 免费欧美性爱视频| 欧美日韩一区二区三区四区五区六区| 国产又爽又黄在线看| 北条麻妃二区三区| 91精品国产偷窥一区二区| 成人一区二区三区四区五区| 91免费福利| 成人久久久久久| 亚洲日韩第一页| 精品色播| 婷婷天堂| 黄色免费在线网站| 亚洲AV成人片色在线观看麻豆| 色视频在线观看免费| 中文字幕一区二区蜜桃| 在线中文字幕AV| 国产一毛a一毛a在线观看| www.99视频| 久久日精品| 熟妇在线观看| 白天操夜夜操| 国产v片| 中文字幕日本无码| 亚洲免费观看高清完整版在va线 | 大鸡巴网站| 亚洲精品成人无码熟妇在线| 无码免费视频在线观看| 大蕉伊人网| www.婷婷五月天| 日韩中文字幕av在线| 99热国产| 久久国际精品| 久草视频免费在线播放| www.17c嫩嫩草色蜜桃网站| 天天做天天爱夜夜爽| 伊人亚洲综合| 日逼网站视频| 青草社区在线观看| 成人黄色av| 东方AV免费在线观看| 日本成人黄色视频| 久久久久久大香蕉| 日韩综合在线| 欧美一级棒| 精品视频在线免费| 无码精品人妻一区二区三区漫画| 高清无码一级片| 国产乱子伦日B视频| 欧美久久久久久| 色综合网址| 东京热一区二区三区| 亚洲色图欧美在线| 亚洲成人自拍| 十八禁黄网站| 超碰日日夜夜| 日韩成人无码AV| 大香蕉在8线| 精品无码一区二区人妻久久蜜桃 | 操逼在线观看| 丁香五月网站| 边摸边插| 国产无码激情视频| 在线观看视频亚洲| 欧美在线观看网站18| 国产精品国产精品国产专区不卡| 国产精品久久久久无码AV| 久久黄色| 男人亚洲天堂| 丁香五月激情啪啪| 国产v片| 激情五月婷婷丁香| 18禁成人A∨片| 国产精品无码无套在线| 黑人精品欧美一区二区蜜桃| 成人色色视频| 青青草精品| 大香蕉尹人在线观看| 国产天堂视频| 91爱爱视频| 久久大陆| 少妇在线视频| 成人做爰黄A片免费看陈冠| 无码人妻精品一区二区蜜桃漫画| 九九九九九九国产| av高清| 嫩草入口| 天堂无码视频在线播放| 秋霞A片| 亚洲AV播放| 欧美精品国产动漫| 强伦轩农村人妻| 黄视频在线观看免费| 西西人体BBBBBB| 午夜aaa| 精品国产三级片| 欧美色图自拍| 国产亚洲久一区二区写真| www.久久久久| 国产成人综合电影| 91精品亚洲| 99reav| 开心五月婷| 欧洲激情网| 在线观看的av网站| 亚洲日韩精品成人无码专区AV| 97福利| 免费视频一区二区三区四区| 看一级黄色视频| 日本三级片免费观看| 男女日比视频| 五月婷婷丁香网| 久草a视频| 91精品婷婷国产综合久久竹菊| 亚洲精品中文字幕乱码三区91 | A天堂视频| 日逼无码视频| 国产成人无码A片V99| 五月天激情综合网| 国产精品久久久久久久牛牛| 欧美视频色| 成人AV免费在线观看| 9l视频自拍蝌蚪9l成人| 一卡二卡在线视频| 国产AV一区二区三区| 十八禁在线播放| 久久大陆| 成人性生交片无码免费看人| 高清无码在线免费观看| 欧美熟妇性爱| 成人精品123| 国产精品尤物| 尻屄视频| 国产欧美精品| 囯产精品久久久久久久久免费无码 | 88AV在线视频| 中文字幕成人免费视频| 99视频在线| 国产一级免费| 99在线视频免费| 精品国产精品三级精品AV网址| 亚洲欧美大香蕉视频网| 久久国产劲爆∧v内射| 大香蕉96| 三区在线观看| 国产在线久久久| 成人免费无遮挡无码黄漫视频| 中文字幕日本在线| 日韩色逼| 男人的天堂视频在线观看| 大香蕉久久视频| 七十路の高齡熟妇无码| 欧美AAA在线观看| 草久热| 精品國產一區二區三區久久蜜月| 日本天堂网| 亚洲国产无码在线| 亚洲精品女人| 亚欧综合在线| 一本大道香蕉av久久精东影业| A片在线视频| 亚洲人天堂| 国产精品成人在线视频| 成人国产精品秘欧美高清| 蜜臀在线视频| 人人摸人人射| 国产三级小视频| 美女裸身18禁| 黄色视频免费在线看| 亚洲三级在线观看| 一区二区三区无码在线观看| 69av在线视频| 大香蕉五月丁香| 在线中文字幕第一页| 奶头和荫蒂添的好舒服囗交漫画| 日韩福利片| 亚洲av免费在线| 久草黄色| 日韩精品视频免费| 在线看V片| 99热国产| 成人不卡| 亚洲无码一区二区三区| 老熟妇搡BBBB搡BBBB| 2018中文字幕第一页| 国产一区在线播放| 久久久久久久久久国产精品免费观看-百度 | 99久久久成人国产精品| 91丨国产丨精品丨丝袜| 亚洲无码在线视频观看| 国产成人无码一区二区在线播放| 在线看一区二区三区| 激情导航| A在线观看| 特黄aaaaaaaa真人毛片| 久操视频网站| 日韩日韩日韩日韩| 成人AV午夜福利| 中国黄色一级A片| 露脸偷拍AV2025| 一区不卡| 国产精品A片守望| 精品国产黄色| 精品伊人| 性欧美欧美巨大69| 欧美va亚洲va| 五月婷婷黄色| 色色色色色色色色欧美| 亚洲热热| 2025国产精品| 97人妻人人揉人人躁人人| 色婷婷国产精品综合在线观看| 色情小电影免费网站观看网址在线播| 亚洲欧美另类图片| 999久久精品| 国产一级一片免费播放放a| 狠狠干在线视频| 美女网站在线观看| 黄色电影网站在线观看| 久久综合操| 男人操女人免费网站| 国产一二三四| 国产一区免费观看| 亚洲午夜久久久之蝌蚪窝| 色77777| 午夜精品无码| 一级a一级a免费观看视频Al明星 | 亚洲欧美日韩电影| 黄色视频在线观看免费网站 | 插插插综合| 免费在线a视频| 99热在线中文字幕| 一级二级无码| 4虎亚洲人成人网www| 日韩视频在线免费观看| 天天撸在线视频| 日逼黄色| 操骚屄视频| 老熟妇一区二区三区啪啪| 亚洲精选中文字幕| 蜜臀AV网| 色香蕉网| 国产成人精品a视频一区| 97超碰网| 三级av网站| 亚洲福利一区二区| 成人午夜大片| 人人摸人人干| 日本免费A片| 色欲AV在线| 国产精品操逼视频| AAA免费视频| 黄色小视频在线免费观看| 成人免费视频国产免费麻豆,| 欧美日逼网| 国产嫩苞又嫩又紧AV在线| 黃色一級片黃色一級片尖叫声-百度-百 | 女人A片一级黄色| www.cao| 九九无码| 青草免费视频| 中文字幕伊人| 欧美日本在线| 午夜精品无码| 色五月婷婷中文字幕| 日皮视频免费| 日韩高清一区二区| 日本A在线| 97香蕉久久国产超碰青草专区 | 日韩黄色三级| 欧美日韩国产成人在线| 日韩伊人网| 麻豆日韩| 亚洲av无码乱码| 日韩黄色免费电影| 免费无码又爽又黄又刺激网站| 毛片导航| 国产内射精品| 青青草精品在线视频| 国产精品99视频| 欧美日韩亚洲天堂| 亚州无码免费| 亚洲AV无码专区在线播放中文| 欧美成人无码一区二区三区| 一区二区三区久久久久| 久色婷婷| 老熟女伦一区二区三区| 精品一区二区三区四| 无码中文一区| 天天撸在线| 午夜福利站| 性爱日韩| 久久久久久免费毛片精品| 人人干日日干| 国产丝袜av| 黑人巨大精品欧美| 亚州中文字幕| 91人妻最真实刺激绿帽| 亚洲免费观看高清完整版在线| 91人妻人人澡人人澡人人精品| 嫩BBB槡BBBB槡BBBB视频-百度 | 99精品免费视频| 亚洲在线视频观看| 成人在线视频观看| 亚洲有码中文字幕| 91在线视频| 俺去搞| 亚洲免费视频播放| 操东北女人逼| 一区二区视频在线| 九色国产视频| 日韩激情无码一区二区| 国产欧美精品一区二区色综合 | 成人国产在线无码AV免费| 男人日女人视频| 久久XXX| 国产精品免费一区二区三区都可以| 依人综合网| 操碰视频在线| а天堂中文在线资源| 国产一级黄色录像| 东京热在线免费观看| 91高潮| 成人无码动漫A片| 日韩免费视频一区二区| 欧美午夜成人| 天天日天天色| 人人操人人摸人人干| 制服.丝袜.亚洲.中文豆花| 大黑人荫蒂BBBBBBBBB| 麻豆性爱视频| 2025AV天堂| 中文字幕乱视频| 日韩欧美高清视频| 国产A片| 天天操天天干欧美精品| 成人乱码一区二区三区| 日本精品在线观看视频| 久久黄片视频| 人人干超碰| 久热免费视频在线观看| 91人妻人人澡人人爽精品| 特级西西444www精品视频| 国产精品无码在线| 亚洲成人av在线观看| 免费一级无码婬片A片APP直播| 乱伦91| 黄网国产手机在线观看| 日本高清视频www| 台湾成人在线视频| 久久久久亚洲精品| 肏逼综合网| 三级片高清无码| 亚洲AV无码一区东京热久久| 日韩欧美A片| 黄色一级录像| 999精品视频| 性爱AV在线观看| 日韩成人在线看| 国产欧美日韩一区| 蜜臀网| 亚洲欧美在线免费观看| 麻豆视频一区| 乱伦视频网站| 成人三级电影| 日韩性爱视屏| 伊人久久大香蕉视频| 国产精品女人777777| 激情五月婷婷综合| 久久草在线播放| 欧美黄色三级片| 国精品91无码一区二区三区在线 | 中文字幕第315页| xxxx国产| 日韩黄色电影在线观看| 亚洲口味重一级黄片| 亚洲精品无码在线播放| 怡红院一区二区| 成人福利视频在线观看| 日本一区二区三区免费观看| 成人av一区| 欧美成人无码片免费看A片秀色| A黄色绿像| 中文字幕在线欧美| 人妻爽爽| 色色影音先锋| 亚洲欧美国产毛片在线| 亚洲无码免费在线| 少妇高潮视频| 亚洲欧美激情视频| www在线视频| 丁香激情五月少妇| JLZZJLZZ亚洲女人| 无码人妻丰满熟妇| 国产精品久久久久久久久免费无码 | 欧美黄色三级片| 日韩电影免费在线观看中文字幕| 久久精品大屁股| 在线免费高清无码| 欧美日韩一区二区在线| www.豆花视频成人版| 日韩免费黄色电影| av天天干| 日韩免费在线观看一区入口 | 久久思热国产| japanese在线观看| 特级西西人体WWWWW| 九九色影院| 国产精品在线观看| 一级黄色影片| 荫蒂添到高潮免费视频| 久久久久蜜桃| 91探花在线观看| 少妇高潮视频| 在线aⅴ| 高清无码视频免费| 91精品无码视频| 婷婷五月精品中文字幕| 亚洲秘无码一区二区三区欧美| 51一区二区三区| 九久热| 91探花秘在线播放偷拍| 成人AV毛片| 欧美三级在线| 亚州在线播放| 中文字幕精品视频在线观看| 微拍福利一区| 亚洲精品自拍| 人妻超碰在线| 五月婷综合| JlZZJLZZ亚洲美女18| 成人性爱视频在线观看| 成人性生活A级毛片网站| 欧美午夜精品久久久久免费视| 精品无码一区二区三区蜜桃李宗瑞| 欧美久久精品| 毛片网站在线观看| 特级特黄A级高潮播放| 大香蕉免费| 91丨九色丨熟女丰满| 亚洲日本在线观看| 欧美视频综合| 青青综合网| 艳妇乳肉豪妇荡乳AV无码福利| 97人妻人人澡| 国产欧美一区二区三区国产幕精品| 欧美日韩高清丝袜| 国产精品揄拍500视频| 日韩一级黄| 毛片在线看片| 日本在线一级| 欧美日韩爱爱| 少妇大战黑人46厘米| 欧美精品无码一区二区| 大香蕉久久伊人| 超碰在线观看99| www.婷婷| 久久国产偷拍| 一区二区三区电影网| 欧美一级aa| 色婷婷AV| www.91在线视频| 亚州天堂网| 无码专区一区二区三区| 久久久18禁一区二区三区精品 | 国产一级片| 欧美最猛黑A片黑人猛交蜜桃视频| 亚洲欧美卡通| 九九热在线观看| 日韩高清无码免费看| 一插菊花网| 婷婷五月天成人电影| 午夜毛片| 国产精品女人777777| 久在线视频| 亚洲性爱一区| 国产精品视频久久久久| 在线观看老湿视频福利| 69人妻人人澡人人爽人人精品| 亚洲Japanese办公室制服| 天天撸天天日| 91足浴店按摩漂亮少妇| 成人在线免费观看视频| 91av在线免费播放| 亚洲免费观看高清完整版在va线观看| 日韩一级欧美一级| www.av免费| 91大神在线看| 免费毛片+一区二区三区| 婷婷综合缴情亚洲另类在线| 婷婷成人视频| 一道本无码在线| 无码欧美| 国产久久性爱| 国产操逼视频网站| 人人摸人人干| 日韩欧美三级在线| 2025国产成人精品一区| 六月婷婷五月丁香| 特级西西444www大胆高清图片| 久久午夜电影| 97黄片| 国产一区二区三区视频| 国产精品在线免费| 激情亚洲婷婷| 欧美激情视频一区二区三区不卡 | 成人福利午夜A片公司| 日韩人妻无码视频| 久久色在线视频| 亚洲www在线| 69精品视频| 激情五月天av| 麻豆国产91在线播放| 亚洲不卡视频| 午夜老司机福利一二三区| 亚洲V国产v欧美v久久久久久 | 日韩在线99| www.777av| 国产在线欧美在线| 伊人黄色片| 超碰人人艹| 性饥渴欧美老妇XXXXX| 一本无码高清| 成人香蕉网| 亚洲aaaaaa| 亚洲精品一区二区三区新线路| 伊人网在线播放| 亚洲欧美在线视频免费| 97人妻在线视频| 欧美性视频网站| AV高清无码在线| 日韩精品五区| 熟女资源站| 久久午夜福利电影| 亚洲无码自拍| 免费看黄色AV| 欧美午夜网站| 天堂网中文在线| 豆花天天吃最新视频| www.超碰| 国产又爽又黄网站免费观看| 免费视频二区| 色综合五月| 国产乱伦中文字幕| 久久精品人妻|