1. 脚手架对比
脚手架 | 命令 | vue-cli2 | vue init webpack hello | vue-cli4(5) | vue create hello-world | cra | npx create-react-app my-app | vite | yarn create vite |
2. vue-cli2
2.1 初始化项目
// 因为是老版本了指定版本 可以使用npx来执行
npm install -g vue-cli@2 --force
vue init webpack hello-vue-cli2
2.2 基本流程
downloading template...
// 1. 选择一些选项
? Project name
vue-cli · Generated "hello-vue-cli2".
// 2. 安装依赖
Installing project dependencies ...
// 3. Project initialization finished!
2.3 基本思路
// 1. 注册命令 vue init
// 2. 下载模板到缓存目录
// 3. 获取用户交互数据 生成项目
// 4.安装依赖
2.4 简单实现
// https://github.com/vuejs/vue-cli/tree/v2
mkdir 1.vue-cli2 && cd 1.vue-cli2
npm init -y
// 安装一些依赖
npm install commander chalk ora inquirer download-git-repo metalsmith handlebars consolidate async minimatch
// https://www.npmjs.com/package/download-git-repo 下载git上仓库代码
const download = require('download-git-repo')
// https://www.npmjs.com/package/commander 命令行
const program = require('commander')
// https://www.npmjs.com/package/ora 显示loading效果
const ora = require('ora')
// https://www.npmjs.com/package/inquirer 用户交互
const inquirer = require('inquirer')
// https://www.npmjs.com/package/chalk 输出彩色文字
const chalk = require('chalk')
// https://www.npmjs.com/package/metalsmith 可插拔的静态站点生成器
const Metalsmith = require('metalsmith')
// https://www.npmjs.com/package/handlebars 模板引擎
const Handlebars = require('handlebars')
// https://www.npmjs.com/package/consolidate 模板引擎的结合体。包括了常用的jade和ejs
const render = require('consolidate').handlebars.render
// https://www.npmjs.com/package/async for use with Node-style callbacks
const async = require('async')
// https://www.npmjs.com/package/minimatch 匹配文件
const match = require('minimatch')
- 注册命令
// 在package.json文件中增加bin
"bin": {
"vue-init": "bin/vue-init",
}
- vue-init.js
// 根据用户选项下载指定模板
// 1. 解析命令行参数
program
.usage('<template-name> [project-name]')
program.parse(process.argv)
// template-name(webpack) vue init webpack hello
const template = program.args[0]
// 项目名 hello
const rawName = program.args[1]
// /Users/liu/.vue-templates/webpack 会在.vue-templates目录下看是否有缓存
const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))
// 2. 拉取template代码 这里只是将template模板拉取下来了 还没有生成我们的项目
const spinner = ora('downloading template')
spinner.start()
const officialTemplate = 'vuejs-templates/' + template
// https://github.com/vuejs-templates/webpack
// 下载模板 webpack 将webpack git clone 到 .vue-templates/webpack中
download(officialTemplate, tmp, { clone }, err => {
spinner.stop()
generate(rawName, tmp, to, err => {
logger.success('Generated "%s".', name)
})
})
// 3. 生成项目 generate 用户交互 处理template目录中的内容
// 最简单的就是不处理用户的选项 直接将整个项目copy到我们的目录中
function generate(name, src, dest, done) {
// 1. 获取选项
const js = path.join(src, "meta.js"); // 找到 vuejs-templates/webpack下的meta.js文件
const req = require(path.resolve(js));
// {metalsmith:{ before: addTestAnswers}, prompts, filters}
const opts = req;
// 2. metalsmith
const metalsmith = Metalsmith(path.join(src, "template"));
const data = Object.assign(metalsmith.metadata(), {
destDirName: name,
inPlace: dest === process.cwd(),
noEscape: true,
});
// 3. 处理用户交互
metalsmith.use(askQuestions(opts.prompts))
// 4. 根据用户的选项过滤一些文件
// vue-cli是filter文件 vue-cli4提供的插件一般是提供最基础的template新增一些文件
.use(filterFiles(opts.filters))
// 5. build成最终的文件
metalsmith
.clean(false)
.source(".") // start from template root instead of `./src` which is Metalsmith's default for `source`
.destination(dest)
.build((err, files) => {
// console.log(err, files)
done(err);
// installDependencies
if (typeof opts.complete === 'function') {
opts.complete(data, {chalk})
}
});
return data;
}
// 3. 处理用户交互
metalsmith
// .use(askQuestions(opts.prompts))
// 获取用户的选项
.use((files, metalsmith, done) => {
// ask(opts.prompts, metalsmith.metadata(), done)
async.eachSeries(
Object.keys(opts.prompts),
(key, next) => {
// 会在metadata中添加属性
prompt(metalsmith.metadata(), key, opts.prompts[key], next);
},
done
);
})
// 4. 根据用户的选项过滤一些文件
// vue-cli是filter文件 vue-cli4提供的插件一般是提供最基础的template新增一些文件
metalsmith.use((files, metalsmith, done) => {
// filter(files, opts.filters, metalsmith.metadata(), done)
const fileNames = Object.keys(files);
Object.keys(opts.filters).forEach((glob) => {
fileNames.forEach((file) => {
if (match(file, glob, { dot: true })) {
const condition = opts.filters[glob];
const fn = new Function(
"data",
"with (data) { return " + condition + "}"
);
if (!fn(metalsmith.metadata())) {
delete files[file];
}
}
});
});
done();
})
- meta.js
// 我们有很多的逻辑是要meta.js中处理的
// https://github.com/vuejs-templates/webpack/blob/develop/meta.js
module.exports = {
metalsmith: {
// When running tests for the template, this adds answers for the selected scenario
before: addTestAnswers
},
prompts: {},
filters: {}
// 完成之后安装依赖
complete: function(data, { chalk }) {
const green = chalk.green
sortDependencies(data, green)
const cwd = path.join(process.cwd(), data.destDirName)
// runCommand(executable, ['install'], {cwd})
// 使用node的spawn const spawn = require('child_process').spawn
// spawn()
installDependencies(cwd, data.autoInstall, green)
.then(() => {
return runLintFix(cwd, data, green)
})
.then(() => {
printMessage(data, green)
})
.catch(e => {
console.log(chalk.red('Error:'), e)
})
},
}
- 处理缓存
// 我们通过template的命令去拉取模板文件 但是当我们本地存在的时候可以直接使用 .vue-template中的资源
if (exists(templatePath)) {
// 当存在的时候就直接 generate而不会去download
generate()
}
5. gitee
```js
// 如果我们需要自己下载git仓库的模板 gitee的api文档
const gitUrl = `https://gitee.com/api/v5/orgs/${org}/repos`;
// gitee可以使用 个人觉得实现不够友好 是对download-git-repo的扩展
// 期待gitee 王圣松 大佬提供更好的npm包
npm install zdex-downloadgitrepo
// 简单实现
// https://gitee.com/vue-next/vue3-cli/tree/v2.0
2.5 npm包发布
// 1. 调试
npm link
ibox-vue-init webpack hello
// 2. 发布npm包
npm login (npm whoami)
npm publish
// You must sign up for private packages
npm publish --access public
3. vite
3.1 初始化项目
// https://cn.vitejs.dev/
// vite不在本文考虑范围内 我们只是分析 vite中 create-vite包的实现
// vite采用了monorepo的管理方式 vite的实现在构建篇分析 这里不分析具体的实现
// 指令会先安装create-vite 执行
yarn create vite
3.2 基本流程
// 1. 输出项目名称
// 2. 选择框架
// 3. 选择variant 是否需要ts
3.3 基本思路
// https://github.com/vitejs/vite/tree/main/packages/create-vite
// create-vite的实现比较简单 就是根据选项直接从项目中拉取文件
template-vue-ts
template-vue
3.4 简单实现
- 初始化项目
// 我们只是简单实现 crete-vite就不使用 lerna初始化项目了
npm init -y
// 增加bin字段
"bin": {
"create-box-vite": "index.js",
"ibox-cva": "index.js"
},
// 注意 ⚠️ 我们要加上files字段
// 安装依赖
npm install minimist prompts kolorist
// https://www.npmjs.com/package/minimist 轻量级的命令行参数解析 commander
const argv = require("minimist")(process.argv.slice(2));
// https://www.npmjs.com/package/prompts 用户交互 inquirer.prompt
const prompts = require("prompts");
// https://www.npmjs.com/package/kolorist 轻量命令行输出工具 chalk
const { blue } = require("kolorist");
- index.js
// 1. 定义我们支持的框架列表 简化为只支持vue和react
const FRAMEWORKS = [
{
name: 'vue',
color: green,
variants: [
{
name: 'vue',
display: 'JavaScript',
color: yellow
},
{
name: 'vue-ts',
display: 'TypeScript',
color: blue
}
]
},
{
name: 'react',
color: cyan,
variants: [
{
name: 'react',
display: 'JavaScript',
color: yellow
},
{
name: 'react-ts',
display: 'TypeScript',
color: blue
}
]
}
]
// [ 'vue', 'vue-ts', 'react', 'react-ts' ]
const TEMPLATES = FRAMEWORKS.map(
(f) => (f.variants && f.variants.map((v) => v.name)) || [f.name]
).reduce((a, b) => a.concat(b), [])
async function init() {
// 2. 等待用户选择结果 projectName framework variant
let result = await prompts([])
const { framework, packageName, variant } = result
template = variant || framework || template
// 3.根据用户的选项找到 对应的 template 模板文件
const templateDir = path.join(__dirname, `template-${template}`)
// 4.将文件遍历copy到我们的目录中
const files = fs.readdirSync(templateDir)
for (const file of files.filter((f) => f !== 'package.json')) {
write(file)
}
// 单独处理package.json文件 要添加name属性
// .gitignore 文件需要 renameFiles .开头的文件会出问题
}
- template文件
// 我们可以用自己的最佳实践 直接拷贝vite的内容
3.5 npm包发布
// i-box/create-vite@0.0.1
// 包名是 @i-box/create-box-vite 不需要create yarn add做了什么?
// 执行 yarn create @i-box/box-vite
4. cra
4.1 初始化项目
// https://github.com/facebook/create-react-app
npx create-react-app react-demo
4.2 基本流程
// 1. 创建项目 mkdir my-app
Creating a new React app in /Users/liu/my-app.
// 2.初始化npm按照依赖
// cd my-app npm init -y
// yarn add react react-dom react-scripts cra-template
Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...
yarn add v1.22.10
[1/4] ? Resolving packages...
[2/4] ? Fetching packages...
[3/4] ? Linking dependencies...
[4/4] ? Building fresh packages...
success Saved lockfile.
├─ cra-template@1.1.2
├─ react-dom@17.0.2
├─ react-scripts@4.0.3
└─ react@17.0.2
info All dependencies
├─ cra-template@1.1.2
├─ immer@8.0.1
├─ react-dev-utils@11.0.4
├─ react-dom@17.0.2
├─ react-scripts@4.0.3
├─ react@17.0.2
└─ scheduler@0.20.2
✨ Done in 20.65s.
// 3.初始化git仓库 git init
Initialized a git repository.
// 4.使用yarn安装模板 cra-template
Installing template dependencies using yarnpkg...
yarn add v1.22.10
[1/4] ? Resolving packages...
[2/4] ? Fetching packages...
[3/4] ? Linking dependencies...
[4/4] ? Building fresh packages...
success Saved lockfile.
success Saved 17 new dependencies.
// 5.安装模版之后将cra-template remove掉
Removing template package using yarnpkg...
yarn remove v1.22.10
[1/2] ? Removing module cra-template...
[2/2] ? Regenerating lockfile and installing missing dependencies...
success Uninstalled packages.
✨ Done in 7.42s.
// 6.创建git的commit
Created git commit.
// 成功
Success! Created my-app at /Users/liu/my-app
Inside that directory, you can run several commands:
// 7. 可以运行的命令 build start
yarn start
Starts the development server.
yarn build
Bundles the app into static files for production.
We suggest that you begin by typing:
cd my-app
yarn start
Happy hacking!
4.3 实现基本思路
// 1. 安装 react react-dom react-scripts cra-template
// 2. 安装 cra-template 模板文件
// 3. 卸载 cra-template
4.4 简单实现
- 项目初始化
// 使用lerna初始化
lerna init
// 创建子项目
// 创建项目 主要分析这个包
lerna create @i-box/create-ibox-react-app
// 模板文件
lerna create @i-box/ibox-cra-template
// 脚本 我们执行 "start": "react-scripts start", webpack最佳实践 暂不分析改部分
lerna create @i-box/ibox-react-scripts
// 在package.json中添加
"workspaces": [
"packages/*"
],
- cra源码调试
// 1. 下载源码 https://github.com/facebook/create-react-app
// 2. 执行yarn 按照依赖 创建链接
// 3. .vscode\launch.json中配置
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch via NPM",
"request": "launch",
"runtimeArgs": ["run-script", "create"],
"runtimeExecutable": "npm",
"skipFiles": ["<node_internals>/**"],
"type": "pwa-node"
}
]
}
// 4. 在package.json中新增脚本
"create": "node ./packages/create-react-app/index.js hello-world",
- create-ibox-react-app
// 主要实现这个包
- cra-template
// 这是一个模板文件 我们可以直接拷贝 cra仓库中的代码
template目录和template.json文件
// 我们需要在package.json中指定files
"files": [
"template",
"template.json"
],
- react-script
// 这是一个比较核心的包 和vue中的vue-cli-service类似
// webpack最佳实践 但是好像不支持插件系统
// react-app-rewired是如何修改webpack配置的
4.5 create-ibox-react-app
- 项目初始化
// 1. 在package.json中新增bin目录
"bin": {
"ibox-react-app": "./index.js",
"ibox-cra": "./index.js"
}
// 2. 安装依赖
yarn workspace @i-box/create-ibox-react-app add chalk commander fs-extra cross-spawn
- index.js
#!/usr/bin/env node
const { init } = require("./createReactApp");
init();
- createReactApp
function init() {
// 1. 解析参数
const program = new commander.Command(packageJson.name)
.version(packageJson.version)
.arguments("<project-directory>")
.usage(`${chalk.green("<project-directory>")} [options]`)
.action((name) => {
projectName = name;
})
.parse(process.argv);
createApp(
projectName,
program.verbose,
program.scriptsVersion,
program.template,
program.useNpm,
program.usePnp
);
}
- createApp
async function createApp(name, verbose, version, template, useNpm, usePnp) {
const root = path.resolve(name);
const appName = path.basename(root);
fs.ensureDirSync(name); // // mkdir my-app
console.log(`Creating a new React app in ${chalk.green(root)}.`);
const packageJson = {
name: appName,
version: '0.1.0',
private: true,
};
// 写入package.json文件
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2) + os.EOL
);
const originalDirectory = process.cwd();
// 切换目录
process.chdir(root);
// 执行run方法 主要就是安装四个包
await run(root, appName, originalDirectory);
}
- run
async function run(root, appName, originalDirectory) {
// 我们先使用官方的包
const scriptName = "react-scripts";
const templateName = "cra-template"; // 模版其实是可以配置的
const allDependencies = ["react", "react-dom", scriptName, templateName];
console.log("Installing packages. This might take a couple of minutes.");
console.log(
`Installing ${chalk.cyan("react")}, ${chalk.cyan(
"react-dom"
)}, and ${chalk.cyan(scriptName)} with ${chalk.cyan(templateName)}`
);
await install(root, allDependencies);
// 安装完之后 create-react-app包的功能基本就完成了
// 接下来要去react-scripts包去执行相关逻辑
let data = [root, appName, true, originalDirectory, templateName];
let source = `
var init = require('react-scripts/scripts/init.js');
init.apply(null, JSON.parse(process.argv[1]));
`;
// 执行node命令 react-script包中scripts目录下的init.js
await executeNodeScript({ cwd: process.cwd() }, data, source);
process.exit(0);
}
// 执行 react-scripts中的脚本文件 后面的步骤也是在这里面处理的
async function executeNodeScript({ cwd }, data, source) {
return new Promise((resolve) => {
const child = spawn(
process.execPath,
["-e", source, "--", JSON.stringify(data)],
{ cwd, stdio: "inherit" }
);
child.on("close", resolve);
});
}
- install
function install(root, allDependencies) {
return new Promise((resolve) => {
const command = "yarnpkg";
// 拼接参数
const args = ["add", "--exact", ...allDependencies, "--cwd", root];
// 执行yarn add spawn跨平台的开启子进程的包
const child = spawn(command, args, { stdio: "inherit" });
child.on("close", resolve);
});
}
4.6 react-scripts
- 初始化项目
// react-script分为两部分来实现
// 1.继续create-react-app run方法中install之后的逻辑 执行 scripts中的init.js
// 2.作为命令来使用 react-scripts start (yarn start) 暂不分析
// 安装依赖
yarn workspace @i-box/ibox-react-scripts add react react-dom
yarn workspace @i-box/ibox-react-scripts add cross-spawn fs-extra chalk webpack webpack-dev-server babel-loader babel-preset-react-app html-webpack-plugin open
// 在package.json文件中配置bin和scripts脚本
"bin": {
"rs": "./bin/react-scripts.js"
},
"files": [
"bin",
"scripts"
],
"scripts": {
"build": "node ./bin/react-scripts build",
"start": "node ./bin/react-scripts start"
},
- scripts中的init.js方法
// 我们需要完成事
// 修改package.json内容(增加脚本命令)
// 拷贝cra-template内容
// 初始化仓库
// 安装cra-template中依赖 template.json文件
// 卸载cra-template
module.exports = function (
appPath, // path.resolve(appName)
appName, // my-app
verbose,
originalDirectory, // process.cwd()
templateName // cra-template
) {
// 拿到项目中的package.json文件
const appPackage = require(path.join(appPath, "package.json"));
// 先拿到模板中的package.json文件 将package.json的内容做一个合并 merge
const templatePath = path.dirname(
require.resolve(`${templateName}/package.json`, { paths: [appPath] })
);
// 在package.json文件中新增几个命令 start build test eject
appPackage.scripts = Object.assign({
start: "react-scripts start",
build: "react-scripts build",
});
// 其他一系列的设置 eslint config browsers list appPackage.xxx = xx
// 写入package.json文件内容
fs.writeFileSync(
path.join(appPath, "package.json"),
JSON.stringify(appPackage, null, 2) + os.EOL
);
// 拷贝文件 拿到cra-template下的template中的内容
const templateDir = path.join(templatePath, "template");
fs.copySync(templateDir, appPath); // 将template中的内容拷贝过来
// template中的gitignore文件 写入到.gitignore中
const data = fs.readFileSync(path.join(appPath, "gitignore"));
fs.appendFileSync(path.join(appPath, ".gitignore"), data);
fs.unlinkSync(path.join(appPath, "gitignore"));
// 初始化仓库 require("child_process").execSync
execSync("git init", { stdio: "ignore" });
console.log("Initialized a git repository.");
// 安装cra-template中的依赖 template.json
let command = "yarnpkg",
remove = "remove",
args = ["add"];
// args = args.concat(["react", "react-dom"]); // old CRA cli
console.log(`Installing template dependencies using ${command}...`);
// 执行安装命令 主要是处理template.json中的文件
const proc = spawn.sync(command, args, { stdio: "inherit" });
// 移除cra-template
console.log(`Removing template package using ${command}...`);
const proc1 = spawn.sync(command, [remove, templateName], {
stdio: "inherit",
});
// 提交git commit
execSync("git add -A", { stdio: "ignore" });
execSync('git commit -m "Initialize project using Create React App"', {
stdio: "ignore",
});
// 完成
console.log(`Success! Created ${appName} at ${appPath}`);
};
4.7 发布
lerna publish
// 需要先提交git
5. vue-cli4
1. 初始化项目
// https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue
// 现在已经是 vue-cli5的beta版本了 主要用了webpack5
vue create hello-world
2. 基本流程
// 1. 选择预设
? Please pick a preset: Manually select features
// 选择手动模式之后选择特性
? Check the features needed for your project:
Choose Vue version, Babel, Router, Vuex, Linter
// 选择了模式之后会多一些选项
? Choose a version of Vue.js that you want to start the project with 3.x
// 2.创建项目 mkdir hello-world
✨ Creating project in /Users/xueshuai.liu/hello-world.
// 3.初始化git仓库
? Initializing git repository...
// 4.安装plugins
⚙️ Installing CLI plugins. This might take a while...
// 5. 调用生成器 这里是核心 插件
? Invoking generators...
// 6.按照额外的依赖
? Installing additional dependencies...
// 7. 运行编辑的钩子hooks
⚓ Running completion hooks...
// 8.生成README.md文件
? Generating README.md...
// 9. get start
? Successfully created project hello-world.
? Get started with the following commands:
3. 简单实现
- 初始化项目
// vue-cli4现在是基于插件系统的
lerna init
// 执行yarn安装依赖
yarn
// 修改package.json文件
"workspaces": ["packages/*"]
// 在lerna.json中新增
"useWorkspaces": true,
// 初始化子项目
lerna create @i-box/cli
lerna create @i-box/cli-service
lerna create @i-box/cli-shared-utils
// 插件全部使用官方提供的
lerna create @i-box/cli-plugin-vuex
// yarn vs lerna
yarn 用来处理依赖
lerna用来初始化和发布
3.1 cli
// 我们主要实现 cli包的逻辑
cd packages/cli
// 增加bin命令
"bin": {
"ibox-vue": "bin/vue.js"
}
// 安装依赖
yarn workspace @i-box/cli add minimist commander fs-extra inquirer isbinaryfile ejs vue-codemod
1. vue.js
// 注册命令
program.command("create <app-name>").action((name, options) => {
require("../lib/create.js")(name, options);
});
program.parse(process.argv);
2. create.js
async function create(projectName, options) {
const cwd = options.cwd || process.cwd()
const inCurrent = projectName === '.'
const name = inCurrent ? path.relative('../', cwd) : projectName
const targetDir = path.resolve(cwd, projectName || '.')
// 获取弹出选项的结果 手动模式下选择的选项 vueVersion babel router vuex等
let promptModules = getPromptModules();
// 初始化 Creator
const creator = new Creator(name, targetDir, promptModules)
// 调用create方法
await creator.create(options)
}
2.1 getPromptModules
// 我们手动模式下选择的特性
const getPromptModules = () => {
return [
'vueVersion',
'babel',
'typescript',
'pwa',
'router',
'vuex',
'cssPreprocessors',
'linter',
'unit',
'e2e'
].map(file => require(`../promptModules/${file}`))
}
3. Creator
- 初始化 new Creator
// 手动模式
const isManualMode = answers => answers.preset === '__manual__'
module.exports = class Creator extends EventEmitter {
// 初始化
constructor(name, context, prompModules) {
super()
this.name = name
this.context = process.env.VUE_CLI_CONTEXT = context
// 获取到选择的预设和特性 如果是default会有默认值
const { presetPrompt, featurePrompt } = this.resolveIntroPrompts()
// 手动模式 最后一步保存配置的位置和是否保存在文件中 会保存在 .vuerc 中
this.outroPrompts = this.resolveOutroPrompts()
// 预设选项
this.presetPrompt = presetPrompt
// 特性的选项 例如我们选择了 eslint会让我们选项哪种规范
this.featurePrompt = featurePrompt
// 选项的特征之后加入的选项
this.injectedPrompts = []
// 完成的回调
this.promptCompleteCbs = []
// 当我们选项了不同的特性 会通过api给我们增加对应的选项
const promptAPI = new PromptModuleAPI(this)
// 遍历特性 promptAPI提供一个注入选择特性 一个选择之后的回调(注入一些插件)
promptModules.forEach((m) => m(promptAPI));
}
// 执行create方法
async create (cliOptions = {}, preset = null) {}
}
- resolveIntroPrompts 获取预设和特性
function resolveIntroPrompts() {
// 我们保存的选项会存在 .vuerc 中
// const savedOptions = loadOptions()
// const preset = Object.assign({}, savedOptions.presets, defaults.presets)
// 默认的预设
const defaultPreset = {
useConfigFiles: false,
cssPreprocessor: undefined,
plugins: {
'@vue/cli-plugin-babel': {},
'@vue/cli-plugin-eslint': {
config: 'base',
lintOn: ['save']
}
}
}
const presets = {
'default': Object.assign({ vueVersion: '2' }, defaultPreset),
'__default_vue_3__': Object.assign({ vueVersion: '3' }, defaultPreset)
}
// 预设的选项 显示使用的
const presetChoices = Object.entries(presets).map(([name, preset]) => {
let displayName = name
if (name === 'default') {
displayName = 'Default'
} else if (name === '__default_vue_3__') {
displayName = 'Default (Vue 3)'
}
return {
// Default ([Vue 2] babel, eslint)
// Default (Vue 3) ([Vue 3] babel, eslint)
// name: `${displayName} (${formatFeatures(preset)})`,
name: `${displayName}`,
value: name
}
})
const presetPrompt = {
name: 'preset',
type: 'list',
message: `Please pick a preset:`,
choices: [
...presetChoices,
// 加上一个手动选择的模式
{
name: 'Manually select features',
value: '__manual__'
}
]
}
// 特性
const featurePrompt = {
name: 'features',
when: isManualMode, // 手动模式下才会有
type: 'checkbox',
message: 'Check the features needed for your project:',
choices: [], // 保存我们的选项
pageSize: 10
}
return {
presetPrompt,
featurePrompt
}
}
- resolveOutroPrompts
// 是否保存本次的选择结果 会在 .vuerc 中保存 什么时候保存的? 在create过程中 promptAndResolvePreset
function resolveOutroPrompts() {
return [
{
name: 'useConfigFiles',
when: isManualMode,
type: 'list',
message: 'Where do you prefer placing config for Babel, ESLint, etc.?',
// 是在单独的文件中还是保存在package.json文件中
choices: [
{
name: 'In dedicated config files',
value: 'files'
},
{
name: 'In package.json',
value: 'pkg'
}
]
},
{
name: 'save',
when: isManualMode,
// 是否保存
type: 'confirm',
message: 'Save this as a preset for future projects?',
default: false
},
{
name: 'saveName',
// 如果保存输入保存的name
when: answers => answers.save,
type: 'input',
message: 'Save preset as:'
}
]
}
- PromptModuleAPI
// 给特性注册一些api 方便选择之后做一些处理
class PromptModuleAPI {
constructor(creator) {
this.creator = creator
}
// 提供一些api来修改选项 还有完成选择之后的回调
// 1. 插入一些新的特性 例如vuex vue的版本
injectFeature (feature) {
this.creator.featurePrompt.choices.push(feature)
}
// 2. 加入新的选择 vueVersion选择了之后会要选择vue2还是vue3
injectPrompt (prompt) {
this.creator.injectedPrompts.push(prompt)
}
// 3. 加入一些选项 我们可以自定义一些插件来提供选项
injectOptionForPrompt (name, option) {
this.creator.injectedPrompts.find(f => {
return f.name === name
}).choices.push(option)
}
// 4. 选项完成之后的回调
onPromptComplete (cb) {
this.creator.promptCompleteCbs.push(cb)
}
}
4. create
// 调用create方法 creator.create
// 执行create方法
async create(cliOptions = {}, preset = null) {
// 1. 获取用户的选项
let preset = await this.promptAndResolvePreset();
// 2. 注入核心的服务 @vue/cli-service 是一个核心的特殊的插件
preset.plugins["@vue/cli-service"] = Object.assign(
{
projectName: name,
},
preset
);
// 3.开始创建项目
log(`✨ Creating project in ${chalk.yellow(context)}.`);
// 4. 根据插件的依赖生成package.json写入 会处理版本的问题
const pkg = {
name,
version: "0.1.0",
private: true,
devDependencies: {},
// ...resolvePkg(this.context)
};
// 在选项完成的回调中我们会增加 plugins的属性 options.plugins['@vue/cli-plugin-vuex'] = {}
const deps = Object.keys(preset.plugins);
deps.forEach((dep) => {
pkg.devDependencies[dep] = "latest";
});
await writeFileTree(context, {
"package.json": JSON.stringify(pkg, null, 2),
});
// 5. 初始化仓库
log(`? Initializing git repository...`);
// execa 执行命令
await this.run("git init");
// 6.安装依赖
log(`⚙\u{fe0f} Installing CLI plugins. This might take a while...`);
await run("npm install");
// 7. 核心 生成器插件
log(`? Invoking generators...`);
const plugins = await this.resolvePlugins(preset.plugins, pkg);
const generator = new Generator(context, {
pkg,
plugins,
afterInvokeCbs,
afterAnyInvokeCbs,
});
await generator.generate({
extractConfigFiles: preset.useConfigFiles,
});
// 8.安装额外的依赖 初始化readme文件 完成
await writeFileTree(context, { "README.md": "README.md" });
await run("git add -A");
log(`? Successfully created project ${chalk.yellow(name)}.`);
}
- promptAndResolvePreset
// 获取用户的选项 解析预设
function promptAndResolvePreset(answers = null) {
if (!answers) {
// await clearConsole(true)
this.injectedPrompts.forEach(prompt => {
const originalWhen = prompt.when || (() => true)
prompt.when = answers => {
return isManualMode(answers) && originalWhen(answers)
}
})
let prompts = [
this.presetPrompt,
this.featurePrompt,
...this.injectedPrompts,
...this.outroPrompts
]
// 获取用户选项
// answers = await inquirer.prompt(this.resolveFinalPrompts())
answers = await inquirer.prompt(prompts)
}
// 是否保存到package.json文件中
// if (answers.packageManager) {
// // fs.writeFile()
// saveOptions({
// packageManager: answers.packageManager
// })
// }
let preset
if (answers.preset && answers.preset !== '__manual__') {
// 不是手动模式下的 可能是默认的default 也可能是上次保存的
// preset = await this.resolvePreset(answers.preset)
preset = defaults.presets
} else {
// 手动模式
preset = {
useConfigFiles: answers.useConfigFiles === 'files',
plugins: {}
}
answers.features = answers.features || []
// 运行模块注册的回调来完成预设 options.vueVersion = answers.vueVersion
this.promptCompleteCbs.forEach(cb => cb(answers, preset))
}
// 保存配置信息到.vuerc文件
// if(answers.save) {}
return preset
}
- resolvePlugins
// 解析插件 插件系统的实现 加载 generator 模块 增加apply方法
// { id: options } => [{ id, apply, options }] 增加apply方法
async resolvePlugins(rawPlugins, pkg) {
// 保证 @vue/cli-service 是第一个插件 这个很重要
// 因为其他的插件是基于里面的template来做修改的
rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true)
const plugins = []
for (const id of Object.keys(rawPlugins)) {
// 加载插件下的 generator 模块
// 插件一般是有一个 generator目录的
// require() Module.createRequire
const apply = loadModule(`${id}/generator`, this.context) || (() => {})
let options = rawPlugins[id] || {}
if (options.prompts) {
let pluginPrompts = loadModule(`${id}/prompts`, this.context)
if (pluginPrompts) {
const prompt = inquirer.createPromptModule()
if (typeof pluginPrompts === 'function') {
pluginPrompts = pluginPrompts(pkg, prompt)
}
if (typeof pluginPrompts.getPrompts === 'function') {
pluginPrompts = pluginPrompts.getPrompts(pkg, prompt)
}
log()
log(`${chalk.cyan(options._isPreset ? `Preset options:` : id)}`)
options = await prompt(pluginPrompts)
}
}
// 增加一个apply方法 什么时候执行的?
plugins.push({ id, apply, options })
}
return plugins
}
5. Generator
// 生成器
class Generator {
constructor(context, { pkg = {}, plugins = [], files = {} } = {}) {
this.pkg = pkg
this.files = {}
this.fileMiddlewares = []; // 插件数组
// load all the other plugins
this.allPlugins = this.resolveAllPlugins()
// @vue/cli-service 插件非常特殊 单独处理
const cliService = plugins.find((p) => p.id === "@vue/cli-service");
this.rootOptions = rootOptions; // 根选项
}
}
- resolveAllPlugins
function resolveAllPlugins() {
const allPlugins = [];
Object.keys(this.pkg.dependencies || {})
.concat(Object.keys(this.pkg.devDependencies || {}))
.forEach((id) => {
if (!isPlugin(id)) return;
const pluginGenerator = loadModule(`${id}/generator`, this.context);
if (!pluginGenerator) return;
allPlugins.push({ id, apply: pluginGenerator });
});
return sortPlugins(allPlugins);
}
- generate方法
async generate ({ extractConfigFiles = false, checkExisting = false} = {}) {
// 1. 初始化插件 遍历执行apply方法
await this.initPlugins()
const initialFiles = Object.assign({}, this.files)
// 单独的配置文件
// this.extractConfigFiles(extractConfigFiles, checkExisting)
// 解析file 执行插件的方法 修改files
await this.resolveFiles()
// 排序
// this.sortPkg()
// 重新生成package.json 插件可能修改了内容
this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
// 写入文件书
await writeFileTree(this.context, this.files, initialFiles, this.filesModifyRecord)
}
- initPlugins
// 初始化插件
function initPlugins() {
const { rootOptions } = this
const pluginIds = this.plugins.map(p => p.id)
for (const plugin of this.plugins) {
const { id, apply } = plugin;
// 给插件提供的一些api方法
const api = new GeneratorAPI(id, this, {}, rootOptions);
// 执行apply方法
await apply(api, options, rootOptions, invoking)
}
}
- GeneratorAPI
// 提供一些api给插件使用
// render渲染内容 extendPackage扩展依赖
// injectImports增加import语句
// injectRootOptions 注入根option等
class GeneratorAPI {
constructor(id, generator, options, rootOptions) {
this.id = id;
this.generator = generator;
this.options = options;
this.rootOptions = rootOptions; // 根选项
this.pluginsData = generator.plugins
.filter(({ id }) => id !== `@vue/cli-service`)
.map(({ id }) => ({
// eslint babel
name: toShortPluginId(id),
}));
// 插件需要往entryFile中添加内容
this._entryFile = undefined;
}
// readeOnly
get entryFile() {
if (this._entryFile) return this._entryFile;
// 这里就指明了 我们的入口是main
const file = fs.existsSync(this.resolve("src/main.ts"))
? "src/main.ts"
: "src/main.js";
return (this._entryFile = file);
}
// api.extendPackage({
// dependencies: {
// 'vue': '^3.0.4'
// },
// devDependencies: {
// '@vue/compiler-sfc': '^3.0.4'
// }
// })
// 插件的功能 修改配置 render渲染内容
extendPackage(fields) {
// 做一个配置的合并 修改package.json文件中的依赖
const pkg = this.generator.pkg;
const toMerge = isFunction(fields) ? fields(pkg) : fields;
for (const key in toMerge) {
const value = toMerge[key];
const existing = pkg[key];
if (
isObject(value) &&
(key === "dependencies" || key === "devDependencies")
) {
pkg[key] = mergeDeps(existing || {}, value);
} else {
pkg[key] = value;
}
}
}
// vue-cli-service中的使用
// api.render('./template', {
// doesCompile: api.hasPlugin('babel') || api.hasPlugin('typescript'),
// useBabel: api.hasPlugin('babel')
// })
// 这一步是没有往files中添加的
render(source, additionalData = {}) {
const baseDir = extractCallDir(); // 提取目录
if (isString(source)) {
source = path.resolve(baseDir, source);
// 添加到fileMiddlewares中
this._injectFileMiddleware(async (files) => {
// 解析额外的数据
const data = this._resolveData(additionalData);
const globby = require("globby"); // 匹配文件
const _files = await globby(["**/*"], { cwd: source, dot: true });
for (const rawPath of _files) {
const targetPath = rawPath
.split("/")
.map((filename) => {
if (filename.charAt(0) === "_" && filename.charAt(1) !== "_") {
return `.${filename.slice(1)}`;
}
return filename;
})
.join("/");
const sourcePath = path.resolve(source, rawPath);
// 使用ejs模版渲染
const content = renderFile(sourcePath, data);
// 如果是buffer
if (Buffer.isBuffer(content) || /[^\s]/.test(content)) {
files[targetPath] = content;
}
}
});
} else if (isObject(source)) {
} else if (isFunction(source)) {
this._injectFileMiddleware(source);
}
}
// 往fileMiddlewares中添加middle 这个时候只是添加了 还没有执行的
// 修改的是fileMiddlewares和pkg files还没有修改
// 执行resolveFiles的时候才会真正执行这些middleware
_injectFileMiddleware(middleware) {
this.generator.fileMiddlewares.push(middleware);
}
// vue-server-cli会使用
hasPlugin(id) {
return this.generator.hasPlugin(id);
}
_resolveData(additionalData) {
return Object.assign(
{
options: this.options,
rootOptions: this.rootOptions,
plugins: this.pluginsData,
},
additionalData
);
}
// Add import statements to a file. vuex vue-router
injectImports(file, imports) {
const _imports =
this.generator.imports[file] ||
(this.generator.imports[file] = new Set());
(Array.isArray(imports) ? imports : [imports]).forEach((imp) => {
_imports.add(imp);
});
}
transformScript(file, codemod, options) {
// debugger;
const normalizedPath = this._normalizePath(file);
this._injectFileMiddleware((files) => {
if (typeof files[normalizedPath] === "undefined") {
error(`Cannot find file ${normalizedPath}`);
return;
}
// files[] =
files[normalizedPath] = runTransformation(
{
path: this.resolve(normalizedPath),
source: files[normalizedPath],
},
codemod,
options
);
});
}
// vue2使用的 注入根选项
injectRootOptions() {
const _options =
this.generator.rootOptions[file] ||
(this.generator.rootOptions[file] = new Set());
(Array.isArray(options) ? options : [options]).forEach((opt) => {
_options.add(opt);
});
}
// 处理文件
postProcessFiles(cb) {
this.generator.postProcessFilesCbs.push(cb);
}
_normalizePath(p) {
if (path.isAbsolute(p)) {
p = path.relative(this.generator.context, p);
}
return p.replace(/\\/g, "/");
}
resolve(..._paths) {
return path.resolve(this.generator.context, ..._paths);
}
}
- resolveFiles
// 执行render方法 增加import语句
async resolveFiles () {
const files = this.files
for (const middleware of this.fileMiddlewares) {
// 执行插件的render方法 使用ejs模版渲染
await middleware(files, ejs.render)
}
// 格式化/和\
// normalizeFilePaths(files)
// handle imports and root option injections
Object.keys(files).forEach(file => {
let imports = this.imports[file]
imports = imports instanceof Set ? Array.from(imports) : imports
if (imports && imports.length > 0) {
files[file] = runTransformation(
{ path: file, source: files[file] },
require('./util/codemods/injectImports'),
{ imports }
)
}
let injections = this.rootOptions[file]
injections = injections instanceof Set ? Array.from(injections) : injections
if (injections && injections.length > 0) {
files[file] = runTransformation(
{ path: file, source: files[file] },
require('./util/codemods/injectOptions'),
{ injections }
)
}
})
}
- injectImports
// 插件vuex需要插入 import store from './store'
// api.injectImports(api.entryFile, `import store from './store'`)
// vue-cli-serve机车的template如下 我们需要插入 import语句 操作ast
// import { createApp } from 'vue'
// import App from './App.vue'
// createApp(App).mount('#app')
function injectImports (fileInfo, api, { imports }) {
const j = api.jscodeshift
const root = j(fileInfo.source)
const toImportAST = i => j(`${i}\n`).nodes()[0].program.body[0]
const toImportHash = node => JSON.stringify({
specifiers: node.specifiers.map(s => s.local.name),
source: node.source.raw
})
const declarations = root.find(j.ImportDeclaration)
const importSet = new Set(declarations.nodes().map(toImportHash))
const nonDuplicates = node => !importSet.has(toImportHash(node))
const importASTNodes = imports.map(toImportAST).filter(nonDuplicates)
if (declarations.length) {
declarations
.at(-1)
// a tricky way to avoid blank line after the previous import
.forEach(({ node }) => delete node.loc)
.insertAfter(importASTNodes)
} else {
// no pre-existing import declarations
root.get().node.program.body.unshift(...importASTNodes)
}
return root.toSource()
}
3.4 cli-shared-utils
// 提供一些公共的方法
yarn workspace @i-box/cli-shared-utils add chalk execa semver chalk strip-ansi readline events ora module
3.3 vue-cli-service
1. 作为核心插件使用
// 根据上面的插件我们可以知道 插件中需要一个generator目录
// 做两件事 一个是render一个是extendPackage
module.exports = (api.options) => {
// 渲染模版
api.render('./template', {})
// 扩展
api.extendPackage({
// 增加脚本文件
scripts: {
'serve': 'vue-cli-service serve',
'build': 'vue-cli-service build'
},
// 增加依赖
dependencies: {
vue: "^3.0.4",
},
devDependencies: {
"@vue/compiler-sfc": "^3.0.4",
},
})
}
// 渲染了 template 所以我们还需要一个template目录在存放文件用来渲染
2. 作为命令使用
// 增加命令 暂不分析
"bin": {
"vue-cli-service": "bin/vue-cli-service.js"
},
// vue-cli-service serve webpack启动服务
// vue-cli-service build 打包
- Service
class Service {
constructor(context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
// 5.0新增的 beat已经用了webpack
checkWebpack(context)
// 获取package.json内容
this.pkg = this.resolvePkg(pkg);
// 插件 会将service的插件放在里面 命令相关的serve build webpack相关的config
this.plugins = this.resolvePlugins(plugins, useBuiltIn)
// vue.config.js中配置chainWebpack 也可以调用api来修改
this.webpackChainFns = [];
// configureWebpack
this.webpackRawConfigFns = [];
// 注册的命令 我们编写的插件就是注册命令
this.commands = {};
}
async run() {}
async init(){}
resolveWebpackConfig(){}
}
// 主要是获取package.json和解析插件
// 找到package.json文件
resolvePkg(inlinePkg, context = this.context) {
// 和之前一样也是用require 找context的package.json文件
Module.createRequire(path.resolve(context, "package.json")).resolve(context)
}
// 解析插件
resolvePlugins(inlinePlugins) {
// 和cli中的插件保持一样 增加apply方法
const idToPlugin = (id, absolutePath) => ({
id: id.replace(/^.\//, "built-in:"),
apply: require(absolutePath || id),
});
let plugins
const builtInPlugins = [
// 命令
"./commands/serve", "./commands/build", "./commands/inspect",
// webpack配置相关的
"./config/base","./config/assets", "./config/css","./config/prod",
].map((id) => idToPlugin(id))
// 找到package.json文件中的plugin
const projectPlugins = Object.keys(this.pkg.devDependencies || {})
.concat(Object.keys(this.pkg.dependencies || {}))
.filter(isPlugin)
.map((id) => idToPlugin(id, resolveModule(id, this.pkgContext)));
return builtInPlugins.concat(projectPlugins)
}
- run
async run() {
// 初始化 加载env load用户配置(vue.config.js) 应用插件
await this.init(mode);
// 执行对应的命令 server中注册命令
let command = this.commands[name];
const { fn } = command;
return fn(args, rawArgv)
}
- init
// init方法主要三个 加载.env文件 加载用户vue.config.js配置文件 应用插件(apply方法提供一个api方法)
async init() {
// 加载.env
// 用户配置的参数必须是 VUE_APP_开头的 webpack中会通过正则匹配 webpack.DefinePlugin定义成全局的变量
this.loadEnv();
// 用户配置 vue.config.js
const userOptions = this.loadUserOptions()
// 应用插件 执行apply方法 config是webpack相关的 混入一些配置
// commands是注册一些命令 加入到this.commands中 run的时候执行对应的命令
const loadedCallback = (loadedUserOptions) => {
this.plugins.forEach(({ id, apply }) => {
// 执行apply方法 传入一些api方法给插件来使用
apply(new PluginAPI(id, this), this.projectOptions);
});
// webpack相关 vue.config.js中配置
this.webpackChainFns.push(this.projectOptions.chainWebpack)
this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
}
return loadedCallback(userOptions)
resolveWebpackConfig()
}
- PluginAPI
// 这些api我们在开发插件的时候可以使用到
// 插件api一个是注册命令 一个是修改配置
class PluginAPI {
// 注册命令 serve build都是这样注册的
registerCommand(name, opts, fn) {
this.service.commands[name] = { fn, opts: opts || {} };
}
// 修改webpack配置
chainWebpack() {
this.service.webpackChainFns.push(fn);
}
configureWebpack() { }
configureDevServer(fn) {}
resolveWebpackConfig() {}
resolveChainableWebpackConfig(){}
}
4. 插件
- 注册命令
program.command("add <plugin> [pluginOptions]").action((plugin) => {
require("../lib/add")(plugin, minimist(process.argv.slice(3)));
});
- add
// vue add native-ui
function add (pluginToAdd, options = {}, context = process.cwd()) {
// 如果有未提交的代码直接return
if (!(await confirmIfGitDirty(context))) { return }
const packageName = resolvePluginId(pluginName)
// yarn add xxx
await pm.add(`${packageName}@${pluginVersion}`)
const generatorPath = resolveModule(`${packageName}/generator`, context)
// const generator = new Generator(
// await generator.generate()
invoke(pluginName, options, context)
}
- api
// GeneratorAPI 给我们提供一些api
// https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli/lib/GeneratorAPI.js
// 插件一般是用 修改package.json文件 增加命令(修改scripts 增加依赖项) 动态注入import render渲染 模版文件等
- 实现一个简单的插件
// 将官方提供的vuex做简单的修改即可
module.exports = (api, options = {}, rootOptions = {}) => {
api.injectImports(api.entryFile, `import native from './plugins/native-ui';`);
api.transformScript(api.entryFile, require("./injectUseNativeUi"));
// 加一个依赖
api.extendPackage({
dependencies: {
"naive-ui": "^2.11.4",
},
});
// 渲染template下的文件
api.render("./template", {});
// 提供选项给用户 部分导入还是全部导入
// if (options.import === "partial") {
// }
};
发表评论
还没有评论,快来抢沙发吧!