highlight: atom-one-dark
theme: scrolls-light
我正在参与掘金会员专属活动-源码共读第一期,点击参与
今天我们来看看vite
的脚手架。
前言
create-vite 主要是用于创建一个项目并根据用户选择配置的
template
将模板文件写入当前创建的目录中。
vite
提供了多个模板及其ts
版本。
重要插件
使用minimist
解析命令行参数
使用prompts
包来实现命令行指引配置的功能。
使用kolorist
包实现不同颜色的关键词。
来看一下这些包的简单使用,方便后续查看create-vite
中的源码。
minimist
通过process.argv
获取命令行参数的字符串数组,前两个值是固定的,第一个是node
程序路径,第二个则是当前执行的文件路径。之后的才是输入的各种参数
const { argv } = process;
console.log('argv', argv);
> node src/index.js -v vv -f
argv [
'/usr/local/Cellar/node@16/16.18.1/bin/node',
'/Users/zgm/Documents/Web/node/node-js/src/index.js',
'-v',
'vv',
'-f'
]
使用minimist
后可以把输入的参数进行解析,使用_
保存命令中的各种参数,当匹配到-
或者--
字符时忽略后边的所有的参数。-
、--
字符后边的命令会添加到对象中,当命令后边有参数(非options
),那么该命令的值就是后边的参数,否则值为true
。
const argv = minimist(process.argv.slice(2), { string: ['_'] }); // _ 中的属性会被转为字符串类型
console.log(argv);
> node src/index.js -v vv -f
{ _: [], v: 'vv', f: true }
源码:
const argv = minimist(process.argv.slice(2), { string: ['_'] });
console.log(argv);
> node src/index.js _asdf 123 321 -v vv -f
{ _: [ '_asdf', '123', '321' ], v: 'vv', f: true }
prompts
轻量级,美观且用户友好的交互式提示库。
单个提示的传入一个对象即可
const prompts = require('prompts');
(async () => {
const response = await prompts({
type: 'text',
name: 'weather',
message: 'What is the weather today?',
});
console.log(response);
})();
多个就需要传入数组
const prompts = require('prompts');
(async () => {
const response = await prompts([
{
type: 'text',
name: 'weather',
message: 'What is the weather today?',
},
{
type: 'confirm',
name: 'out',
message: 'Are you going out for fun now?',
},
]);
console.log(response);
})();
动态prompts
当type
为null
时可以跳过当前这个prompt
。
当type
为一个函数时,有 3 个参数,第一个是上一条prompt
的值,第二个是之前所有prompts
的键值组成的对象,第三个则是当前prompt
对象。
(async () => {
const response = await prompts([
{
type: 'text',
name: 'weather',
message: 'What is the weather today?',
},
{
type: 'confirm',
name: 'out',
message: 'Are you going out for fun now?',
},
{
type: (pre) => (pre ? 'text' : null),
name: 'fun',
message: 'Have fun then',
},
]);
console.log(response);
})();
kolorist
用于输出不同颜色的字符。
const { yellow, green, cyan, blue } = require('kolorist');
console.log(yellow('yellow'));
console.log(green('green'));
console.log(cyan('cyan'));
console.log(blue('blue'));
源码解析 create-vite/index.js
源码可以去 clone 若川
大大的相关仓库
配置项目名
获取命令后的第一个参数默认为项目名,当未获取到参数时提示用户进行配置,否则跳过进行下一步。
// 命令行第一个参数,替换反斜杠 / 为空字符串
let targetDir = formatTargetDir(argv._[0]); // 如果字符串最后一个字符是 '/',则去除
// 命令行参数 --template 或者 -t
let template = argv.template || argv.t;
const defaultTargetDir = 'vite-project';
// 获取项目名
const getProjectName = () => (targetDir === '.' ? path.basename(path.resolve()) : targetDir);
交互解析
try {
result = await prompts(
[
// 项目名
{
type: targetDir ? null : 'text',
name: 'projectName',
message: reset('Project name:'), // reset() 是 kolorist 中的 api
initial: defaultTargetDir, // 默认为 vite-project
onState: (state) => {
targetDir = formatTargetDir(state.value) || defaultTargetDir;
},
},
// 判断是否需要重写目录(是否为空文件夹)
{
type: () => (!fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'confirm'),
name: 'overwrite',
message: () =>
(targetDir === '.' ? 'Current directory' : `Target directory "${targetDir}"`) +
` is not empty. Remove existing files and continue?`,
},
{
type: (_, { overwrite } = {}) => {
// 用户不要重写
if (overwrite === false) {
throw new Error(red('✖') + ' Operation cancelled');
}
return null;
},
name: 'overwriteChecker',
},
// 验证项目名
{
type: () => (isValidPackageName(getProjectName()) ? null : 'text'),
name: 'packageName',
message: reset('Package name:'),
initial: () => toValidPackageName(getProjectName()),
validate: (dir) => isValidPackageName(dir) || 'Invalid package.json name',
},
// 选择框架模板
{
type: template && TEMPLATES.includes(template) ? null : 'select',
name: 'framework',
message:
typeof template === 'string' && !TEMPLATES.includes(template)
? reset(`"${template}" isn't a valid template. Please choose from below: `)
: reset('Select a framework:'),
initial: 0,
choices: FRAMEWORKS.map((framework) => {
const frameworkColor = framework.color;
return {
title: frameworkColor(framework.name),
value: framework,
};
}),
},
// 选择模版对应的语言如 ts
{
type: (framework) => (framework && framework.variants ? 'select' : null),
name: 'variant',
message: reset('Select a variant:'),
// @ts-ignore
choices: (framework) =>
framework.variants.map((variant) => {
const variantColor = variant.color;
return {
title: variantColor(variant.name),
value: variant.name,
};
}),
},
],
{
onCancel: () => {
throw new Error(red('✖') + ' Operation cancelled');
},
},
);
} catch (cancelled) {
console.log(cancelled.message);
return;
}
删除已有文件夹/新建文件夹
// user choice associated with prompts
const { framework, overwrite, packageName, variant } = result;
// 目录
const root = path.join(cwd, targetDir);
if (overwrite) {
// 删除文件夹
emptyDir(root);
} else if (!fs.existsSync(root)) {
// 新建文件夹
fs.mkdirSync(root, { recursive: true });
}
// 递归删除文件夹,相当于 rm -rf xxx。
function emptyDir(dir) {
if (!fs.existsSync(dir)) {
return;
}
for (const file of fs.readdirSync(dir)) {
fs.rmSync(path.resolve(dir, file), { recursive: true, force: true });
}
}
写入模板文件
// determine template
template = variant || framework || template;
console.log(`\nScaffolding project in ${root}...`);
// 获取模板对应文件夹
const templateDir = path.resolve(fileURLToPath(import.meta.url), '..', `template-${template}`);
const write = (file, content) => {
// 这里的 renameFiles,是因为在某些编辑器或者电脑上不支持.gitignore。
const targetPath = renameFiles[file] ? path.join(root, renameFiles[file]) : path.join(root, file);
// 如果要针对某个文件做自定义写入
if (content) {
fs.writeFileSync(targetPath, content);
} else {
copy(path.join(templateDir, file), targetPath);
}
};
const files = fs.readdirSync(templateDir);
for (const file of files.filter((f) => f !== 'package.json')) {
write(file);
}
/* package.json 文件单独处理 */
// 将 json 数据转为 js 对象
const pkg = JSON.parse(fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8'));
// 修改项目名为输入的或文件夹名
pkg.name = packageName || getProjectName();
// 将修改后的内容转为 json 格式,写入文件
write('package.json', JSON.stringify(pkg, null, 2));
/* --------- */
/* 打印安装完成后的信息 */
// 获取包管理软件的信息
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent);
const pkgManager = pkgInfo ? pkgInfo.name : 'npm';
console.log(`\nDone. Now run:\n`);
// 如果命令行不在项目根目录
if (root !== cwd) {
console.log(` cd ${path.relative(cwd, root)}`);
}
switch (pkgManager) {
case 'yarn':
console.log(' yarn');
console.log(' yarn dev');
break;
default:
console.log(` ${pkgManager} install`);
console.log(` ${pkgManager} run dev`);
break;
}
console.log();
延伸函数 copy && copyDir
如果是文件夹用 copyDir 拷贝
function copy(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
copyDir(src, dest);
} else {
fs.copyFileSync(src, dest);
}
}
/**
* @param {string} srcDir
* @param {string} destDir
*/
function copyDir(srcDir, destDir) {
fs.mkdirSync(destDir, { recursive: true });
for (const file of fs.readdirSync(srcDir)) {
const srcFile = path.resolve(srcDir, file);
const destFile = path.resolve(destDir, file);
copy(srcFile, destFile);
}
}
知识拓展:resolve 和 join 的区别
resolve 和 join 都用来进行路径片段的连接,但是区别有两点:
- resolve 会生成绝对路径,而 join 只是返回当前连接的路径。
- resolve 会以最后出现的 ‘/’为起点,作为根路径,忽略前面的片段,而 join 不会。
console.log(path.resolve()) // returns /workspace/demo
console.log(path.resolve('')) // returns /workspace/demo
console.log(path.resolve(\_\_dirname)) // returns /workspace/demo
console.log(path.resolve('/img/books', '/net')) // returns '/net'
console.log(path.resolve('img/books', '/net')) // returns '/net'
console.log(path.resolve('img/books', './net')) // returns '/workspace/demo/img/books/net'
console.log(path.resolve('/img/books', './net')) // returns '/img/books/net'
console.log(path.resolve('/img/books', 'net')) // returns '/img/books/net'
console.log(path.resolve('/img/books', '../net')) // returns '/img/net'
console.log(path.resolve('src','/img/books', '../net')) // returns '/img/net'
console.log(path.resolve('src','./img/books', '../net')) // returns '/workspace/demo/src/img/net'
console.log(path.resolve('src','img/books', '../net')) // returns '/workspace/demo/src/img/net'
path.join('/img', 'book', 'net/abc', 'inter', '..'); // returns /img/book/net/abc
console.log(path.join('/img/books', '../net')) // returns /img/net
console.log(path.join('img/books', '../net')) // returns img/net
console.log(path.join('/img/books', './net')) // returns /img/books/net
console.log(path.join('img/books', './net')) // returns img/books/net
console.log(path.join('/img/books', 'net')) // returns /img/books/net
console.log(path.join('img/books', 'net')) // returns /img/books/net
console.log(path.join('/img/books', '/net')) // returns /img/books/net
console.log(path.join('img/books', '/net')) // returns img/books/net
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
暂无评论内容