如何实现一个轻量级的前端部署工具

496 阅读2分钟

hello,大家好,我是叶小秋,今天给大家分享一个前端部署工具。

不知道现在你是否还在使用手工部署前端项目,或者是你的团队中并没有一个简单、方便、快速的前端部署工具。

如果是这样的话,你可以尝试一下 ye-deploy

npm:www.npmjs.com/package/ye-…

项目仓库:github.com/MrYeZiqing/…

视频.gif

从上面可以看到,仅输入一个 ye-deploy 命令,就完成了一个前端项目的部署,那他是如何实现的呢?

话不多说,我们来动手实践一下

首先的话,我们来搭建一个项目的基础配置

这里我使用 father-build 作为项目构建工具,配置也很简单,npm i father-build 下载,在项目根目录创建 .fatherrc 配置文件

截屏2022-07-18 下午10.04.49.png

在 package.json 中配置打包命令

截屏2022-07-18 下午10.29.20.png

当然你这里你也可以使用其他构建工具(gulp、rollup、webpack)

然后我们创建一下项目的基础目录

截屏2022-07-18 下午10.34.14.png

到这里项目的基础配置已经完成了,接下来就是代码的实现了

我们先来想一下,如果要实现一个前端部署工具,我们要分几步走?

  1. 首先我们需要读取用户配置
  2. 校验用户配置是否正确
  3. 进行项目打包
  4. 连接服务器,将打包产物上传到服务器
  5. 针对动态部署的,需要执行一些服务器的命令
  6. 部署完成

我们先来看,读取用户配置。在src/utils/get-user-config.ts 下创建一个获取用户配置的方法

import {isAbsolute,resolve} from 'path'
import {existsSync} from 'fs'

import getExistFile from './get-exist-file'

const CONFIG_FILES = [
    '.ye-deploy.js'
]

interface YOpts {
    cwd:string,
    customPath?:string
}

export default function getUserConfig(opts:YOpts){
    const {cwd,customPath} = opts

    let finalPath = ''

    if(customPath){
        finalPath = isAbsolute(customPath)?customPath:resolve(cwd,customPath)
        if(!existsSync(finalPath)){
            throw new Error(`找不到配置文件:${customPath}`)
        }
    }

    const configFile = finalPath || getExistFile({cwd,files:CONFIG_FILES})
    if(!configFile){
        throw new Error('找不到配置文件')
    }

    const userConfig = require(configFile)

    return userConfig
}

读取用户配置后,我们需要校验一下用户的参数是否正确

在这之前我们先来看一下我们的一个配置参数

export interface YOpts {
    host:string, // 服务器地址
    port:string | number, // 端口
    username:string, // 服务器用户名
    password:string, // 服务器密码
    privateKey:string, // 服务器密钥
    passphrase:string, // 密钥密码
    distPath:string, // 本地打包文件目录
    webDir:string, // 服务器上部署的地址
    script:string, // 项目打包命令
    delDistFile:boolean, // 部署完成后是否删除打包文件
    config?:string, // 配置文件地址
    cwd:string,
    useUploadValidate?:(itemPath:string)=>boolean, // 上传过程中 过滤某些文件
    useUploadDone?:(command)=> Promise<void> | void, // 上传完成后允许用户自定义一些操作
}

如何去校验参数配置呢?

这里我使用schema-utils来进来校验,在src/utils下创建schema.ts 文件,用于描述校验参数

// https://www.npmjs.com/package/schema-utils
export default {
    type:"object",
    properties:{
        host:{
            type:"string"
        },
        port:{
            anyOf:[
                {
                    type:"string",
                },
                {
                    type:"number",
                }
            ]
        },
        username:{
            type:"string"
        },
        password:{
            type:"string"
        },
        privateKey:{
            type:"string"
        },
        passphrase:{
            type:"string"
        },
        distPath:{
            type:"string"
        },
        webDir:{
            type:"string"
        },
        script:{
            type:"string"
        },
        delDistFile:{
            type:"boolean"
        },
        config:{
            type:"string"
        },
    },
    additionalProperties:false
}

具体使用看 schema-utils 的官方文档

校验完参数之后,我们需要去进行项目打包,可以看到我们的配置参数里面有一个script,这个是项目的打包命令,我们可以借助 child_process exec 来开启一个子线程,进行项目打包

// 项目打包
async runBuild() {
    const spinner = ora('项目打包中...').start()
    try {
        await new Promise((resolve,reject)=>{
            exec(this.options.script,(err)=>{
                if(err){
                    reject(err)
                }else {
                    resolve(true)
                }
            })
        })
        spinner.succeed('项目打包成功')
    }catch(err){
        spinner.fail('项目打包失败,请检查项目配置,重新部署!')
        console.log(err);
        process.exit()
    }
}

项目打包完成之后,我们需要把打包后的产物上传到服务器,这个如何去做呢?

这里我们可以借助 node-ssh模块,通过他去把打包的产物上传到服务器

    // 连接服务器
    async connectSSH() {
        const spinner = ora('正在连接服务器...').start()
        try {
            await this.ssh.connect(this.options)
            spinner.succeed('服务器连接成功')
        }catch(err){
            spinner.fail('服务器连接失败!')
            console.log(err);
            process.exit()
        }
    }

    // 上传到服务器
    async uploadServer() {
        await this.clearOldFile()

        try {
            await this.ssh.putDirectory(this.options.distPath,this.options.webDir,{
                recursive:true,
                concurrency:10,
                validate:(itemPath)=>{
                    // 强制禁止 node_modules这些文件上传
                    const baseName = basename(itemPath)
                    const prohibitFiles = ['node_modules']
                    if(prohibitFiles.includes(baseName)){
                        return false
                    }
                    if(this.options.useUploadValidate){
                        return this.options.useUploadValidate(itemPath)
                    }
                    return true
                },
                tick:(localPath,remotePath,error)=>{
                    if(error){
                        console.log('上传失败',localPath)
                        this.failed.push({localPath,remotePath})
                    }else {
                        console.log('上传成功',localPath);
                    }
                }
            })
            this.uploadServerAgain()
        }catch(err){
            ora().fail('部署失败!')
            throw new Error(err)
        }

    }

上传完成后,对于我们静态的部署其实已经是完成了,但如果要进行动态的部署该如何处理呢?比方说去部署一个 docker 应用

这个时候我们可以借助 node-ssh 的execCommand去执行一些服务器的命令,定义一个 useUploadDone 方法,在上传完成之后触发,useUploadDone 传入 封装后的execCommand,这样子用户就可以在上传完成之后,去执行一些服务器上的命令,比方说,打包 docker镜像。

// command 命令操作
async runCommand({command,cwd,log=true}:{ command:string,cwd?:string,log?:boolean}){
        return new Promise(async (resolve,reject)=>{
            await this.ssh.execCommand(command, { cwd: cwd || this.options.webDir,
                onStdout:(chunk)=>{
                    if(log){
                        console.log(chunk.toString('utf8'))
                    }
                },
                onStderr:(chunk)=>{
                    const errText = chunk.toString('utf8')
                    if(log){
                        console.log(errText)
                    }
                    reject(errText)
                }
            })
            resolve(true)
        })
}

// 上传完成
async uploadDone(){
    // 上传完成后允许用户自定义一些操作
    if(this.options.useUploadDone){
        await this.options.useUploadDone(this.runCommand.bind(this))
    }
    // 断开连接
    this.ssh.dispose()
    if(this.options.delDistFile){
        rimraf.sync(this.options.distPath)
    }
}

之后就是输出部署完成信息

async doneMessage(startTime:Date,endTime:Date){

    ora().succeed(`开始时间:${startTime.toLocaleString()}`)
    ora().succeed(`结束时间:${endTime.toLocaleString()}`)
    const time = Math.round((endTime.getTime() - startTime.getTime()) / 1e3)
    ora().succeed(`总耗时:${time}s`)

    ora().succeed('部署成功!')
    console.log(`项目部署地址: ${this.options.webDir}`)
}

至此,我们的前端部署工具就完成了

接下来就可以发布npm仓库了

完成代码可以看:github.com/MrYeZiqing/…

你也可以直接npm上 下载 ye-deploy 使用

ye-deploy 定位是一个轻量级的前端部署工具,如果你或者你的团队需要一个规范标准的前端部署工具,我个人还是推荐使用像 Jenkins 这些。