手写vue 脚手架

项目地址
项目模板地址

必备模块

  • commander:参数解析 --help借助它
  • inquirer :交互式命令行工具 ,使用可以实现命令行选择功能
  • download-git-repo:在git中下载模板
  • metalsmith:读取所有文件,实现模板渲染
  • consolidate:统一模板引擎

git相关知识

  • 查看每次提交的信息git loggit showgit show 某个制定的commitId

  • 创建tag并与commit建立连接git tag 版本名 commitid

  • 查看所有标签git tag

  • 将tag同步到远程服务器git push -u origin main 标签名

  • 删除taggit tag -d tag名

  • GitHub API调用

    1. 查看某个用户下面所有的仓库

      https://api.github.com/users/meet-you-123/repos
      
    2. 查看某个用户下面特定的仓库信息

      https://api.github.com/repos/用户名/仓库名
      
    3. 查看某个特定仓库的版本信息

      https://api.github.com/repos/用户名/仓库名/tags
      

项目开始

1.目录结构
├── bin 
│ └── www // 全局命令执行的根文件 
├── package.json 
├── src 
│ ├── constants.js // 存放常量
│ ├── create.js // create命令逻辑 
│ ├── config.js // config命令逻辑 
│ ├── main.js // 入口文件 
│ └── utils // 存放工具方法 
│── .huskyrc // git hook 
│── .eslintrc.json // 代码规范校验
2.初始化项目
yarn init -y

// package.json
"bin":{
    "tsc_cli":"./bin/www"
}

// ./bin/www.js
#! /usr/bin/env node //执行环境是node
require("../src/main.js")

//链接包到全局
yarn link
3.commander
yarn add commander

// ./src/main.js
const programe=require("commander")
programe.version("1.0.0").parse(process.env)

执行tsc_cli --help

4. 动态获取版本号
// ./src/constants.js
const {version}=require("../package.json")
module.exports={
    version
}
// ./src/main.js
const programe=require("commander")
const {version}=require("./utils/constants")
programe.version(version).parse(process.env)
5.配置指令命令
// ./src/main.js

//配置三个指令命令
const mapActions={
    create:{
        alias:"c",
        description:"create a project",
        examples:[
            "tsc_cli create <project-name>"
        ]
    },
    config:{
        alias:"conf",
        description:"config project variable",
        examples:[
            "tsc_cli configm set <k><v>",
            "tsc_cli config get <k>"
        ]
    },
    "*":{
        alias:"",
        description:"command not found",
        examples:[]
    }
}

//循环创建命令
Reflect.ownKeys(mapActions).forEach((action)=>{
    programe
    .command(action)//配置指令名称
    .alias(mapActions[action].alias)//命令别名
    .description(mapActions[action].description)//命令的描述
    .action(()=>{
        //访问不到对应的命令 就打印找不到命令
        if(action==="*"){
            console.log(mapActions[action].description)
        }else{
            console.log(action)
        }
    })
})

//监听用户的help事件
programe.on("--help",()=>{
    console.log("\nExamples:")
    Reflect.ownKeys(mapActions).forEach((action)=>{
        mapActions[action].examples.forEach((example)=>{
            console.log(`${example}`)
        })
    })
})
6.create命令
// ./src/main.js
.action(()=>{
    if(action==="*"){
        console.log(mapActions[action].description)
    }else{
        require(path.resolve(__dirname,action))(...process.argv.slice(3))
    }
})


// ./src/create.js
module.exports=async (proName)=>{
    console.log(proName)
}

执行tsc_cli create project可以打印出project

7.获取所有项目模板的所有名字
yarn add axios

// ./src/create.js
const axios=require("axios")
const fetchRepoList=async()=>{
    //获取项目组织中所有的仓库信息,这个项目存放的都是项目模板
    const {
        data
    }=await axios.get("https://api.github.com/users/meet-you-123/repos")
    
    return data
}

module.exports=async ()=>{
    let repos=await fetchRepoList()
    repos=repos.map((itme)=>item.name)
    console.log(repos)
}
8.列出项目名字,并选择inquirer&ora
//注意ora的版本问题
yarn add inquirer ora

// ./src/create.js
const ora=require("ora")
const Inquirer=require("inquirer")
module.exports=async()=>{
    const spinner=ora("fetching template ....")
    spinner.start()
    let repos=await fetchRepoList()
    spinner.succeed()
    repos=repos.map((item)=>item.name)
    const {
        repo
    }=await Inquirer.prompt({
        name:"repo",//获取后选择的结果
        type:"list",//什么方式显示在命令行
        message:"please choice a template to create project",//提示信息
        choices:repos,//选择的数据
    })
    console.log(repo)
}
9.获取版本信息
// ./src/create.js
//封装等待函数
const waitFetchAddLoadinng=(fn,message)=>async(...args)=>{
    const spinner=ora(message);
    spinner.start()
    const result=await fn(...args)
    spinner.succeed()
    return result
}

const fetchTagList=async(repo)=>{
    const {
        data
    }=await axios.get(`https://api.github.com/repos/meet-you-123/${repo}/tags`)
    return data
}
let tags=await waitFetchAddLoadinng(fetchTagList,"fetching tags.....")(repo)
tags=tags.map((item)=>item.name)
const {
    tag
}=await Inquirer.prompt({
        name:"tag",//获取后选择的结果
        type:"list",//什么方式显示在命令行
        message:"please choice a template to create project",//提示信息
        choices:tags,//选择的数据
    })
10.下载项目
// ./src/constants.js
const downloadDirectory=`${process.env[process.platform==="darwin"?"HOME":"USERPROFILE"]}/.template`
module.exports={
    downloadDirectory
}

yarn add download-git-repo

// ./src/create.js
const {promisify}=require("util")
let downloadGitRepo=require("download-git-repo")
downloadGitRepo=promisify(downloadGitRepo)//转成promise方法去使用
const {downloadDirectory}=require("./constants")

const download=async(repo,tag)=>{
    let api=`meet-you-123/${repo}`
    if(tag){
        api+=`#${tag}`
    }
    const dest=`${downloadDirectory}/${repo}`
    await downloadGitRepo(api,dest)
    return dest
}

const target=await waitFetchAddLoadinng(download,'download template....')(repo,tag)


yarn add ncp//拷贝文件功能

let ncp=require("ncp")
ncp=promisify(ncp)
await ncp(result, path.resolve(proname))
11.模板编译
//模板目录
├── tem  //里面存放的是要下载的模板
│   ├── package.json//定制模板中的package.json
├── meta.js
├── package.json

//在模板中添加mate.js以用来询问用户需要的信息值
const prompts = [{
        name: "name",
        type: "input",
        required: true,
        message: "Project name"
    },
    {
        name: "author",
        type: "input",
        message: "Author",
    },
    {
        name: "router",
        type: "confirm",
        message: "Install vue-router"
    },
    {
        name: "store",
        type: "confirm",
        message: "Install vuex"
    },
    {
        name: "vant",
        type: "confirm",
        message: "Install vant"
    }
]
const filter = {   //是否需要过滤的文件
    "src/router/**/*": "router",
    "src/store/**/*": "store",
    "src/plugins/**/*": "vant"
}
module.exports = {
    prompts,
    filter
}

//例如package.json
{
  "name": "{{ name }}",
  "version": "1.0.0",
  "main": "index.js",
  "repository": "https://github.com/meet-you-123/vue_ts_cli_template.git",
  "author": "{{ author }}",
  "license": "MIT",
  "scripts": {
    "dev": "cross-env envMode=dev webpack serve --config ./build/webpack.dev.conf.js  --color",
    "build": "cross-env envMode=prod webpack --config build/webpack.prod.conf.js  --color",
    "build:test": "cross-env envMode=test webpack --config build/webpack.prod.conf.js  --color"
  },
  "devDependencies": {
    "@babel/core": "^7.16.0",
    "@babel/plugin-proposal-class-properties": "^7.16.0",
    "@babel/plugin-proposal-object-rest-spread": "^7.16.0",
    "@babel/plugin-transform-runtime": "^7.16.0",
    "@babel/preset-env": "^7.16.0",
    "@babel/preset-typescript": "^7.16.0",
    "@vue/compiler-sfc": "^3.2.21",
    "autoprefixer": "^10.4.0",
    "babel-loader": "^8.2.3",
    "babel-plugin-import": "^1.13.3",
    "cross-env": "^7.0.3",
    "css-loader": "^6.5.1",
    "dotenv": "^10.0.0",
    "html-webpack-plugin": "^5.5.0",
    "less": "^4.1.2",
    "less-loader": "^10.2.0",
    "postcss-loader": "^6.2.0",
    "style-loader": "^3.3.1",
    "ts-loader": "^9.2.6",
    "typescript": "^4.4.4",
    "vue-loader": "^16.8.3",
    "webpack": "^5.62.1",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.4.0"
  },
  "dependencies": {
    "@babel/runtime": "^7.16.0",
    "@babel/runtime-corejs3": "^7.16.0",
    "axios": "^0.24.0",
    "core-js": "^3.19.1",
    "mustache": "^4.2.0",
    {{#vant}}
    "vant": "^3.2.8",
    {{/vant}}
    "vue": "^3.2.21",
    {{#router}}
    "vue-router": "4",
    {{/router}}
    {{#store}}
    "vuex": "^4.0.2"
    {{/store}}
  }
}

    
//在项目中安装handlebars、consolidate模块
yarn add handlebars consolidate

//在./src/create.js中引入
const {
    promisify
} = require("util")
let Handlebars = require("consolidate").handlebars
Handlebars = promisify(Handlebars)// 包装渲染方法
const MetalSmith = require('metalsmith'); // 遍历文件夹     
    if (!fs.existsSync(path.join(result, "meta.js"))) {
        await ncp(result, path.resolve(proname))
    } else {
        // 复杂模板
        await new Promise((resolve, reject) => {
            MetalSmith(__dirname)
                .source(result) //遍历下载的目录
                .destination(path.resolve(proname)) //最终编译好的文件存放位置
                .use(async (files, metal, done) => {
                    const {
                        prompts,
                        filter
                    } = require(path.join(result, "meta.js"))
                    const obj = await Inquirer.prompt(prompts)
                    console.log(obj);
                    const meta = metal.metadata()
                    Object.assign(meta, obj) //将询问的结果放到metadata中保证在下一个中间件中可以获取到
                    delete files["meta.js"]
                    const fileNames = Object.keys(files)
                    Object.keys(filter).forEach(glob => {
                        glob = "tem/" + glob
                        fileNames.forEach(file => {
                            file = file.replace(/\\/g, "/");
                            if (match(file, glob, {
                                    dot: true
                                })) {
                                console.log('匹配文件');
                                glob = glob.replace("tem/", "")
                                file = file.replace(/\//g, "\\");
                                const condition = filter[glob];
                                if (!obj[condition]) {
                                    delete files[file];
                                }
                            }
                        })
                    })
                    done()
                })
                .use((files, metal, done) => {
                    const obj = metal.metadata()
                    Reflect.ownKeys(files).forEach(async (file) => {
                        if (file.includes(".json") || file.includes(".js") || file.includes(".ts") || file.includes(".vue")) {
                            let content = files[file].contents.toString()
                            if (content.includes("{{")) {
                                content = await Handlebars.render(content, obj)
                                files[file].contents = Buffer.from(content)
                            }
                        }
                    })
                    done()
                })
                .build(err => {
                    if (err) {
                        reject()
                    } else {
                        resolve()
                    }
                })
        })
    }

//注意meta.js和要渲染的文件不能在同一级文件夹中  
//使用require("consolidate").handlebars进行渲染,而不是Mustache
//删除文件注意路径中的符号是否一致,要不然匹配不上
//这里的逻辑就是上面描述的那样,实现了模版替换!到此安装项目的功能就完成了。
项目发布
nrm use npm  // 准备发布包
npm addUser  // 填写账号密码
npm publish  // 已经发布成功
Logo

前往低代码交流专区

更多推荐