欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

代替Vue?Cli的全新腳手架工具create?vue示例解析

 更新時間:2022年10月27日 14:07:55   作者:Harexs  
這篇文章主要為大家介紹了代替Vue?Cli的全新腳手架工具create?vue示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

前言

美國時間 2021 年 10 月 7 日早晨,Vue 團隊等主要貢獻者舉辦了一個 Vue Contributor Days 在線會議,蔣豪群(知乎胖茶,Vue.js 官方團隊成員,Vue-CLI 核心開發(fā)),在會上公開了create-vue,一個全新的腳手架工具。

create-vue 使用 npm init vue 一行命令就能快速的創(chuàng)建基于Vite的Vue3項目

npm init

$ npm init vue

以前我們初始化Vue-Cli項目時太多通過全局的形式安裝, 然后通過vue create project-name命令進行項目安裝,為什么npm init 也可以可以直接初始化一個項目且不需要全局安裝?

本質(zhì)是 npx 命令的語法糖,它在調(diào)用時是會轉(zhuǎn)換為npx 命令

npm init vue@next -> npx create-vue@next
npm init @harexs -> npx @harexs/create
npm init @harexs/test -> npx @harexs/create-test

看完這三個例子應該就明白了 npm init 的作用了

npx

從本地或者遠程npm包運行命令

npx 就是一種調(diào)用npm包的命令,如果沒提供-c或者--call命令則默認從我們指定的包中,查找package.json中的 bin字段,從而確定要執(zhí)行的文件

{
      "name": "create-vue",
      "version": "3.3.4",
      "description": "An easy way to start a Vue project",
      "type": "module",
      "bin": {
        "create-vue": "outfile.cjs" //關鍵
      }
  }

npm init vue 完整的解析就是 本地或者遠程尋找 create-vue 這個包,然后查找package.jsonbin字段值的可執(zhí)行文件,最終就是運行了outfile.cjs這個文件.

源碼

這里使用川哥的create-vue-analysis倉庫,倉庫的版本便于我們學習其思路和實現(xiàn),對應的是3.0.0-beta.6版本。 最新版本已經(jīng)到了3.3.4, 但核心功能以及實現(xiàn)思路是不變的.

倉庫地址 create-vue-analysis

主流程入口

//index.js
async function init() {
 ///
}
init().catch((e) => {
  console.error(e)
})

先不看其他部分, 先關注入口這里 就是 自調(diào)用了異步函數(shù) init

獲取參數(shù)

const cwd = process.cwd() //獲取當前運行環(huán)境項目目錄
//process.argv.slice(2) 用來獲取 npx create-vue 后面?zhèn)魅氲膮?shù) 值為數(shù)組
//minimist 用來格式化獲取傳入的參數(shù)
const argv = minimist(process.argv.slice(2), {
    alias: {
      typescript: ['ts'],
      'with-tests': ['tests', 'cypress'],
      router: ['vue-router']
    },
    // all arguments are treated as booleans
    boolean: true
  })
  //通過minimist獲取的argv結(jié)果是個對象,通過對象屬性去判斷 是否有傳入?yún)?shù)
  const isFeatureFlagsUsed =
    typeof (argv.default || argv.ts || argv.jsx || argv.router || argv.vuex || argv.tests) ===
    'boolean'
  //argv._ 這個_ 屬性獲取的是 沒有-或者--開頭的參數(shù) 
  //比如 npm init create-vue xxx --a 那么argv就是 {_:['xxx'],a:true}
  let targetDir = argv._[0] // argv._[0] 假如對應{_:['xxx'],a:true} 就是 xxx
  //給一會的選項用的 默認項目名稱 defaultProjectName 默認取targetDir
  const defaultProjectName = !targetDir ? 'vue-project' : targetDir
  const forceOverwrite = argv.force

接著是第二部分,主要就是獲取 運行目錄 以及 判斷 命令調(diào)用時 有沒有傳入指定參數(shù)

對話選項

try {
result = await prompts(
      [
        {
        //name 參數(shù)就是一會要收集的對應變量
          name: 'projectName',
          //判斷targetDir 有沒有值 有值的話 就是null  null會跳過這個對話
          type: targetDir ? null : 'text',
          message: 'Project name:',
          initial: defaultProjectName, //默認結(jié)果值 獲取參數(shù)部分已經(jīng)說過這個變量了
          //onState 完成回調(diào),讓targetDir 取 用戶輸入的內(nèi)容 沒輸入直接回車的話 取defaultProjectName
          onState: (state) => (targetDir = String(state.value).trim() || defaultProjectName)
        },
        {
          name: 'shouldOverwrite',
          //canSafelyOverwrite 判斷是否是空目錄并可以寫入 否則判斷有沒有參數(shù)--force 目錄
          // 有一個條件有效就為null 就跳過寫入對話, 否則為confirm 確認框 y/n 
          type: () => (canSafelyOverwrite(targetDir) || forceOverwrite ? null : 'confirm'),
          message: () => {
          //提示文本
            const dirForPrompt =
              targetDir === '.' ? 'Current directory' : `Target directory "${targetDir}"`
            return `${dirForPrompt} is not empty. Remove existing files and continue?`
          }
        },
        {
          name: 'overwriteChecker',
          //檢查是否寫入, type這里的函數(shù) prev 是上一個選項的值, values 是整個對象
          //如果 shouldOverwrite 階段 type變?yōu)? confirm  并且還選了 no 
          //那么這一階段判斷后就會直接退出 不再執(zhí)行 拋出異常
          type: (prev, values = {}) => {
            if (values.shouldOverwrite === false) {
              throw new Error(red('?') + ' Operation cancelled')
            }
            return null
          }
        },
        {
          name: 'packageName',
          //正則驗證 是否符合 package.json name 的值,不符合則讓用戶輸入
          type: () => (isValidPackageName(targetDir) ? null : 'text'),
          message: 'Package name:',
          //沒輸入則取默認值 將targetDir 通過函數(shù)轉(zhuǎn)換為符合packageName的格式
          initial: () => toValidPackageName(targetDir),
          //校驗函數(shù),如果 用戶輸入的包名無法通過 則提示Invalid package.json name 重新輸入
          validate: (dir) => isValidPackageName(dir) || 'Invalid package.json name'
        },
        {
          name: 'needsTypeScript',
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add TypeScript?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        },
        {
          name: 'needsJsx',
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add JSX Support?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        },
        {
          name: 'needsRouter'
          //toggle 和confirm 無異  isFeatureFlagsUsed 如果有指定某一參數(shù) 則跳過后面所有對話,
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add Vue Router for Single Page Application development?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        },
        {
          name: 'needsVuex',
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add Vuex for state management?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        },
        {
          name: 'needsTests',
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add Cypress for testing?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        }
      ],
      {
        onCancel: () => {
          throw new Error(red('?') + ' Operation cancelled')
        }
      }
    )
  } catch (cancelled) {
    console.log(cancelled.message)
    process.exit(1)
  }

這一部分 使用了prompts這個庫, 它提供了 命令行對話選項的能力, 這里主要收集用戶的選擇以及輸入,

默認值

//取出前面對話選項后的值, 如果沒有的話 就取 argv上的默認值
const {
    packageName = toValidPackageName(defaultProjectName),
    shouldOverwrite,
    needsJsx = argv.jsx,
    needsTypeScript = argv.typescript,
    needsRouter = argv.router,
    needsVuex = argv.vuex,
    needsTests = argv.tests
  } = result
   //root為 命令運行位置 + targetDir 得到 項目路徑
  const root = path.join(cwd, targetDir)
  //如果之前判斷的文件目錄可寫入 則執(zhí)行一次emptyDir
  if (shouldOverwrite) {
    emptyDir(root)
     // 再判斷目錄不存在則創(chuàng)建這個目錄
  } else if (!fs.existsSync(root)) {
    fs.mkdirSync(root)
  }
  console.log(`\nScaffolding project in ${root}...`)
  const pkg = { name: packageName, version: '0.0.0' }
    //往root目錄  寫入初始化 package.json文件
  fs.writeFileSync(path.resolve(root, 'package.json'), JSON.stringify(pkg, null, 2))

emptyDir函數(shù)

function emptyDir(dir) {
  postOrderDirectoryTraverse(
    dir,
    (dir) => fs.rmdirSync(dir),
    (file) => fs.unlinkSync(file)
  )
}

emptyDir 內(nèi)部調(diào)用 postOrderDirectoryTraverse 函數(shù),它來自utils下 我們接著看

export function postOrderDirectoryTraverse(dir, dirCallback, fileCallback) {
  //fs.readdirSync(dir) 返回一個數(shù)組 包含當前目錄下的文件名 列表
  for (const filename of fs.readdirSync(dir)) {
    //遍歷列表 得到 文件的完整路徑
    const fullpath = path.resolve(dir, filename)
    if (fs.lstatSync(fullpath).isDirectory()) {
      // 如果這個文件 也是個目錄 則遞歸繼續(xù)遍歷
      //因為刪除目錄的話 必須要先刪除所有文件
      postOrderDirectoryTraverse(fullpath, dirCallback, fileCallback)
      //執(zhí)行記dirCallback 回調(diào) 也就是fs.rmdirSync(dir) 移除目錄
      dirCallback(fullpath)
      continue
    }
    //否則調(diào)用第二個回調(diào) 就是移除文件
    fileCallback(fullpath)
  }
}

emptyDir 就是對目錄 遞歸遍歷,遇到目錄就繼續(xù)遞歸遍歷然后刪除目錄,文件就直接刪除

模板寫入

const templateRoot = path.resolve(__dirname, 'template')
  const render = function render(templateName) {
    const templateDir = path.resolve(templateRoot, templateName)
    renderTemplate(templateDir, root)
  }
  // Render base template
  render('base')
  // Add configs.
  if (needsJsx) {
    render('config/jsx')
  }
  if (needsRouter) {
    render('config/router')
  }
  if (needsVuex) {
    render('config/vuex')
  }
  if (needsTests) {
    render('config/cypress')
  }
  if (needsTypeScript) {
    render('config/typescript')
  }
   // Render code template.
  // prettier-ignore
  const codeTemplate =
    (needsTypeScript ? 'typescript-' : '') +
    (needsRouter ? 'router' : 'default')
  render(`code/${codeTemplate}`)
  // Render entry file (main.js/ts).
  if (needsVuex && needsRouter) {
    render('entry/vuex-and-router')
  } else if (needsVuex) {
    render('entry/vuex')
  } else if (needsRouter) {
    render('entry/router')
  } else {
    render('entry/default')
  }

先看第一部分

// work around the esbuild issue that `import.meta.url` cannot be correctly transpiled
  // when bundling for node and the format is cjs
  // const templateRoot = new URL('./template', import.meta.url).pathname
  const templateRoot = path.resolve(__dirname, 'template')
  //需要區(qū)分的是  templateDir取的是 對應當前執(zhí)行文件環(huán)境中的文件地址
  // root變量 path.join(cwd, targetDir) process.cwd() 也就是取的命令執(zhí)行時的地址
  //到時候?qū)目赡芫褪沁@樣:
  //C:xxx/xxxx/npm-cache/_npx/xxxx/.bin/create-vue/template  
  //D:/xxx/projectDir/vue-project 
const render = function render(templateName) {
    const templateDir = path.resolve(templateRoot, templateName)
    renderTemplate(templateDir, root)
  }

需要注意 __dirname 是不存在會報錯的,作者在注釋也留有信息, 因為我們的項目環(huán)境是ESM,原先CJS的環(huán)境變量不能用了, 所以我們要換成這種寫法

import path from "node:path";
import url from "node:url";
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
console.log(__filename, __dirname);

node:xxx 這種寫法是Node提供的, 我在 promiseify 文章中也有講到過

接下里看 renderTemplate函數(shù)

./utils/renderTemplate.js

function renderTemplate(src, dest) {
//src  是 npx拉取到create-vue本地緩存目錄中的 模板目錄地址
//dest 是 用戶命令執(zhí)行時的 項目地址
  const stats = fs.statSync(src)
  //statSync 返回一個文件類對象 可以看下面的截圖
  if (stats.isDirectory()) {
    // if it's a directory, render its subdirectories and files recusively
    //如果src 是一個目錄  則 在對應dest 的位置創(chuàng)建一個目錄 
    //recursive: true 允許遞歸創(chuàng)建目錄 也就是允許 a/b/c 這種形式來創(chuàng)建
    fs.mkdirSync(dest, { recursive: true })
    for (const file of fs.readdirSync(src)) {
     //遍歷src中所有文件, 遞歸自身,傳入的參數(shù)變?yōu)?src/file 也即是每個文件名
      //第二個參數(shù) 對應的就是 dest/file
      renderTemplate(path.resolve(src, file), path.resolve(dest, file))
    }
    return
  }
   //這一步就是循環(huán)遍歷 將src中的每個文件包目錄都寫入到 我們聲明的project(dest)項目中
    //如果src不是一個目錄 則來到這里 先取出文件名
  const filename = path.basename(src)
    //判斷文件名是否為 package.json 并且 dest 也對應存在這個文件
  if (filename === 'package.json' && fs.existsSync(dest)) {
    // merge instead of overwriting
    //讀取兩個package.json的內(nèi)部
    const existing = JSON.parse(fs.readFileSync(dest))
    const newPackage = JSON.parse(fs.readFileSync(src))
     //合并兩個文件的內(nèi)并 并重新排序  得到新的 package.json內(nèi)容
    const pkg = sortDependencies(deepMerge(existing, newPackage))
    //重新寫入到 dest下
    fs.writeFileSync(dest, JSON.stringify(pkg, null, 2) + '\n')
    return
  }
  if (filename.startsWith('_')) {
    // rename `_file` to `.file`
    //resolve 合并名字
    //dirname 取目錄名字
    dest = path.resolve(path.dirname(dest), filename.replace(/^_/, '.'))
  }
//拷貝文件
  fs.copyFileSync(src, dest)
}

接下來解析合并以及排序package.json的工具函數(shù)

//判斷是不是對象
const isObject = (val) => val && typeof val === 'object'
//合并數(shù)組值 通過new Set 去除重復項
const mergeArrayWithDedupe = (a, b) => Array.from(new Set([...a, ...b]))
/**
 * Recursively merge the content of the new object to the existing one
 * @param {Object} target the existing object
 * @param {Object} obj the new object
 */
function deepMerge(target, obj) {
  for (const key of Object.keys(obj)) {
    const oldVal = target[key]
    const newVal = obj[key]
    if (Array.isArray(oldVal) && Array.isArray(newVal)) {
      //合并數(shù)組項
      target[key] = mergeArrayWithDedupe(oldVal, newVal)
    } else if (isObject(oldVal) && isObject(newVal)) {
      //如果是對象則遞歸自身 繼續(xù)遍歷
      target[key] = deepMerge(oldVal, newVal)
    } else {
      //否則直接覆蓋值
      target[key] = newVal
    }
  }
  return target
}
export default deepMerge

比較簡單的深拷貝函數(shù)

export default function sortDependencies(packageJson) {
  // packageJson json的內(nèi)容對象
  // sorted排序字段
  const sorted = {}
  //需要排序的類型
  const depTypes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
  for (const depType of depTypes) {
    //如果json中包含了 這個字段
    if (packageJson[depType]) {
      //賦值sorted對應的 depType 為空對象
      sorted[depType] = {}
      Object.keys(packageJson[depType]) //得到packageJson depType所有key的數(shù)組
        .sort() //使用默認排序
        .forEach((name) => {
          //遍歷這個key的數(shù)組  然后將對應的值 重新賦值到sorted depType name
          sorted[depType][name] = packageJson[depType][name]
        })
    }
  }
  //ES6 展開對象語法 重復的key 會被覆蓋 達到排序的效果  
  return {
    ...packageJson,
    ...sorted
  }
}

sortDependencies 核心就是通過聲明指定字段數(shù)組 然后取出來給到新的對象,然后通過ES6的展開語法 同樣的key 新key覆蓋舊key的應用實現(xiàn)了排序效果

又學到一招,展開語法真好使啊~

 // Add configs.
  if (needsJsx) {
    render('config/jsx')
  }
  if (needsRouter) {
    render('config/router')
  }
  if (needsVuex) {
    render('config/vuex')
  }
  if (needsTests) {
    render('config/cypress')
  }
  if (needsTypeScript) {
    render('config/typescript')
  }
  // Render code template.
  // prettier-ignore
  const codeTemplate =
    (needsTypeScript ? 'typescript-' : '') +
    (needsRouter ? 'router' : 'default')
  render(`code/${codeTemplate}`)
  // Render entry file (main.js/ts).
  if (needsVuex && needsRouter) {
    render('entry/vuex-and-router')
  } else if (needsVuex) {
    render('entry/vuex')
  } else if (needsRouter) {
    render('entry/router')
  } else {
    render('entry/default')
  }

在看這一段,基本能明白主要是在做什么操作。通過前面收集的變量進行render

 if (needsTypeScript) {
    // rename all `.js` files to `.ts`
    // rename jsconfig.json to tsconfig.json
    //前面說過 preOrderDirectoryTraverse函數(shù),前面是通過遍歷的形式 去移除文件和目錄
    //這里的調(diào)用只傳入第二個參數(shù)就是針對非目錄的文件
    preOrderDirectoryTraverse(
      root,
      () => {},
      (filepath) => {
        //看作者的注釋也很好理解,遇到.js結(jié)尾文件 重寫為ts
        if (filepath.endsWith('.js')) {
          fs.renameSync(filepath, filepath.replace(/\.js$/, '.ts'))
          //遇到jsconfig.json 重寫為 tsconfig.json
        } else if (path.basename(filepath) === 'jsconfig.json') {
          fs.renameSync(filepath, filepath.replace(/jsconfig\.json$/, 'tsconfig.json'))
        }
      }
    )
    // Rename entry in `index.html
    //取到 index.html 文件路徑
    const indexHtmlPath = path.resolve(root, 'index.html')
    //通過utf-8 讀取 文件內(nèi)容
    const indexHtmlContent = fs.readFileSync(indexHtmlPath, 'utf8')
    //將原本引用的 main.js 重寫為 main.ts  這里也很好理解
    fs.writeFileSync(indexHtmlPath, indexHtmlContent.replace('src/main.js', 'src/main.ts'))
  }
export function preOrderDirectoryTraverse(dir, dirCallback, fileCallback) {
  for (const filename of fs.readdirSync(dir)) {
    //遍歷dir
    const fullpath = path.resolve(dir, filename)
    if (fs.lstatSync(fullpath).isDirectory()) {
      dirCallback(fullpath)
      //如果是目錄 則 調(diào)用dirCallback
      // in case the dirCallback removes the directory entirely
      //執(zhí)行完后再判斷 目錄是否還存在 再遞歸自身繼續(xù)調(diào)用
      if (fs.existsSync(fullpath)) {
        preOrderDirectoryTraverse(fullpath, dirCallback, fileCallback)
      }
      continue
    }
    fileCallback(fullpath)
  }
}

preOrderDirectoryTraverse稍有不同,對于目錄級的回調(diào)會先執(zhí)行然后再判斷目錄是否還存在,在進行遞歸

 if (!needsTests) {
    // All templates assumes the need of tests.
    // If the user doesn't need it:
    // rm -rf cypress **/__tests__/
    preOrderDirectoryTraverse(
      root,
      (dirpath) => {
        //對于目錄 得到目錄名
        const dirname = path.basename(dirpath)
        //如果目錄名為cypress || __tests__
        if (dirname === 'cypress' || dirname === '__tests__') {
          emptyDir(dirpath) //執(zhí)行 清空目錄操作
          fs.rmdirSync(dirpath) //最后移除這個目錄
        }
      },
      () => {}
    )
  }
  //通過npm_execpath來獲取當前執(zhí)行的包管理器絕對路徑
  //得到路徑后 通過 關鍵 字符去匹配
  const packageManager = /pnpm/.test(process.env.npm_execpath)
    ? 'pnpm'
    : /yarn/.test(process.env.npm_execpath)
    ? 'yarn'
    : 'npm'
  // README generation
  //生產(chǎn)README  generateReadme 也比較簡單不展開講它了
  fs.writeFileSync(
    path.resolve(root, 'README.md'),
    generateReadme({
      projectName: result.projectName || defaultProjectName,
      packageManager,
      needsTypeScript,
      needsTests
    })
  )
  console.log(`\nDone. Now run:\n`)
  if (root !== cwd) {
    console.log(`  ${bold(green(`cd ${path.relative(cwd, root)}`))}`)
  }
  console.log(`  ${bold(green(getCommand(packageManager, 'install')))}`)
  console.log(`  ${bold(green(getCommand(packageManager, 'dev')))}`)
  console.log()

簡述

至此,整個源碼就解析完畢了,剛開始確實感覺很復雜,畢竟Node很多API都不是很熟悉, 然后就一一拆解開來對著文檔慢慢看了,讀源碼還是很需要耐心~

大體的流程:

  • 收集用戶指定的參數(shù) 以及 項目名
  • 通過對話選項卡確定用戶的配置
  • 根據(jù)對話選項卡后用戶的配置匹配模板目錄下的文件,一一寫入到項目文件夾中
  • 再判斷是否需要Ts/測試 對文件做修改
  • 生成其他文件 流程結(jié)束

快照

項目中還有個snapshot.js文件, 它主要是通過const featureFlags = ['typescript', 'jsx', 'router', 'vuex', 'with-tests']組合生成 31種加上default共計 32種組合,然后通過子線程命令spawnSync 調(diào)用 bin 然后循環(huán)把 我們組合好的 參數(shù)傳給它執(zhí)行,也就是相當于執(zhí)行了npm init vue這一步操作并傳入組合好的參數(shù)

最后生成不同的模板在 playground目錄中

關于這個命令: spawnSync

總結(jié)

在寫完本文的時候,把源碼大概梳理了2-3遍, 讀源碼很需要耐心, 遇到不懂的API還要翻文檔, 但是等你完全弄明白里面的原理和實現(xiàn)思路之后,就有一種油然而生的開心, 而且以后要開發(fā)類似的腳手架時 也可以有一定的思路和想法去實現(xiàn)它!

以上就是代替Vue Cli的全新腳手架工具create vue示例解析的詳細內(nèi)容,更多關于Vue Cli腳手架工具create vue的資料請關注腳本之家其它相關文章!

相關文章

最新評論