您现在的位置是:网站首页> 编程资料编程资料
代替Vue Cli的全新脚手架工具create vue示例解析_vue.js_
2023-05-24
501人已围观
简介 代替Vue Cli的全新脚手架工具create vue示例解析_vue.js_
前言
美国时间 2021 年 10 月 7 日早晨,Vue 团队等主要贡献者举办了一个 Vue Contributor Days 在线会议,蒋豪群(知乎胖茶,Vue.js 官方团队成员,Vue-CLI 核心开发),在会上公开了create-vue,一个全新的脚手架工具。
create-vue 使用 npm init vue 一行命令就能快速的创建基于Vite的Vue3项目
npm init
$ npm init vue

以前我们初始化Vue-Cli项目时太多通过全局的形式安装, 然后通过vue create project-name命令进行项目安装,为什么npm init 也可以可以直接初始化一个项目且不需要全局安装?
本质是 npx 命令的语法糖,它在调用时是会转换为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 就是一种调用npm包的命令,如果没提供-c或者--call命令则默认从我们指定的包中,查找package.json中的 bin字段,从而确定要执行的文件
{ "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.json中 bin字段值的可执行文件,最终就是运行了outfile.cjs这个文件.
源码
这里使用川哥的create-vue-analysis仓库,仓库的版本便于我们学习其思路和实现,对应的是3.0.0-beta.6版本。 最新版本已经到了3.3.4, 但核心功能以及实现思路是不变的.
主流程入口
//index.js async function init() { /// } init().catch((e) => { console.error(e) }) 先不看其他部分, 先关注入口这里 就是 自调用了异步函数 init
获取参数
const cwd = process.cwd() //获取当前运行环境项目目录 //process.argv.slice(2) 用来获取 npx create-vue 后面传入的参数 值为数组 //minimist 用来格式化获取传入的参数 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结果是个对象,通过对象属性去判断 是否有传入参数 const isFeatureFlagsUsed = typeof (argv.default || argv.ts || argv.jsx || argv.router || argv.vuex || argv.tests) === 'boolean' //argv._ 这个_ 属性获取的是 没有-或者--开头的参数 //比如 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 接着是第二部分,主要就是获取 运行目录 以及 判断 命令调用时 有没有传入指定参数
对话选项
try { result = await prompts( [ { //name 参数就是一会要收集的对应变量 name: 'projectName', //判断targetDir 有没有值 有值的话 就是null null会跳过这个对话 type: targetDir ? null : 'text', message: 'Project name:', initial: defaultProjectName, //默认结果值 获取参数部分已经说过这个变量了 //onState 完成回调,让targetDir 取 用户输入的内容 没输入直接回车的话 取defaultProjectName onState: (state) => (targetDir = String(state.value).trim() || defaultProjectName) }, { name: 'shouldOverwrite', //canSafelyOverwrite 判断是否是空目录并可以写入 否则判断有没有参数--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这里的函数 prev 是上一个选项的值, values 是整个对象 //如果 shouldOverwrite 阶段 type变为 confirm 并且还选了 no //那么这一阶段判断后就会直接退出 不再执行 抛出异常 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 通过函数转换为符合packageName的格式 initial: () => toValidPackageName(targetDir), //校验函数,如果 用户输入的包名无法通过 则提示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 如果有指定某一参数 则跳过后面所有对话, 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) //如果之前判断的文件目录可写入 则执行一次emptyDir if (shouldOverwrite) { emptyDir(root) // 再判断目录不存在则创建这个目录 } 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函数
function emptyDir(dir) { postOrderDirectoryTraverse( dir, (dir) => fs.rmdirSync(dir), (file) => fs.unlinkSync(file) ) } emptyDir 内部调用 postOrderDirectoryTraverse 函数,它来自utils下 我们接着看
export function postOrderDirectoryTraverse(dir, dirCallback, fileCallback) { //fs.readdirSync(dir) 返回一个数组 包含当前目录下的文件名 列表 for (const filename of fs.readdirSync(dir)) { //遍历列表 得到 文件的完整路径 const fullpath = path.resolve(dir, filename) if (fs.lstatSync(fullpath).isDirectory()) { // 如果这个文件 也是个目录 则递归继续遍历 //因为删除目录的话 必须要先删除所有文件 postOrderDirectoryTraverse(fullpath, dirCallback, fileCallback) //执行记dirCallback 回调 也就是fs.rmdirSync(dir) 移除目录 dirCallback(fullpath) continue } //否则调用第二个回调 就是移除文件 fileCallback(fullpath) } } emptyDir 就是对目录 递归遍历,遇到目录就继续递归遍历然后删除目录,文件就直接删除
模板写入
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') //需要区分的是 templateDir取的是 对应当前执行文件环境中的文件地址 // root变量 path.join(cwd, targetDir) process.cwd() 也就是取的命令执行时的地址 //到时候对应的可能就是这样: //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) renderTemp
相关内容
- 使用Vue写一个todoList事件备忘录经典小案例_vue.js_
- hansontable在vue中的基本使用教程_vue.js_
- vue3使用element ui的方法实例_vue.js_
- React Hook中的useState函数的详细解析_React_
- JavaScript实现echarts水球图百分比展示大屏可视化_javascript技巧_
- 基于Vue3文件拖拽上传功能实现_vue.js_
- Vue中的v-for列表循环示例详解_vue.js_
- vue2源码解析之全局API实例详解_vue.js_
- Ajv format校验使用示例分析_javascript技巧_
- 详解如何用JavaScript编写一个单元测试_javascript技巧_
点击排行
本栏推荐
