记录一下。

项目背景

上文的代码,是我基于egg.js做的一个傻瓜版发布版本工具。机制如下:根据前端请求的参数(一个项目列表,每个项目有一些指令),然后后端根据不同指令和项目id去跑对应的逻辑。

  • 1.创建临时目录
  • 2.拉取指定仓库最新代码
  • 3.自动安装依赖
  • 4.npm run build
  • 5.ftp/git同步代码到生产环境。

界面以及核心代码

在这里插入图片描述
在这里插入图片描述
下面是核心代码。通过长链接做项目发布的整个流程前后端交互方式.代码随便瞎写的,凑合看。
egg.js + bootstrap样式库 + socket.io.js +axios。

后端业务核心代码

'use strict';
const Controller = require('egg').Controller;
const build = require('@root/config/build');
const fs = require('fs');
const shell = require('shelljs');
const FtpService = require('@root/lib/ftp');
const utils = require('@root/lib/utils');
const Log = require('@root/lib/log');
const logCtx = Log.getInstance();

class ProcessController extends Controller {

  constructor(props) {
    super(props);
    this.releasePath = '';
    this.id = '';
    this.tempDirName = '';
    this.buildConfig = build.config;
    this.logFilePath = '';
  }

  log({ msg, id, toast = true, showLine = true } = {}) {
    if (typeof arguments[0] === 'string')msg = arguments[0];
    if (!id)id = this.id;
    if (!id) return;
    const { ctx } = this;
    ctx.socket.emit('log', ctx.helper.parseMsg('process-log', { id, msg, toast, showLine }));
  }

  recordFileLog({ msg }) {
    Log.recordFileLog({ filePath: this.logFilePath, msg });
  }


  async index() {
    const { ctx } = this;
    const { action, id } = ctx.args[0];
    // 重要
    this.id = id;
    const taskId = '';
    const projectInfoData = await this.service.project.getProjectById(id);
    const projectInfo = utils.objTranslate(projectInfoData);
    if (action === 'release') {
      try {
        const startTime = new Date().getTime();
        await ctx.helper.sleep(100);
        // 先说流程已经启动
        this.log('流程已启动......');

        // 1.创建虚拟目录
        const tempDirPath = this.buildConfig.tempDirPath;

        const nowTime = new Date().getTime();
        const tempDirName = `${projectInfo.repository_name}_${nowTime}_${id}`;
        const tempPath = `${tempDirPath}/${tempDirName}`;
        this.releasePath = tempPath;
        this.tempDirName = tempDirName;

        this.logFilePath = `${this.releasePath}/${projectInfo.repository_name}_${this.tempDirName}_${this.id}_log.txt`;

        this.log({ msg: `打包路径:${tempPath}`, toast: false });
        fs.mkdirSync(`${tempPath}`, { recursive: true });

        this.log('目录创建成功...' + tempPath);
        shell.cd(tempPath);
        // 2.拉取代码
        const { repository_url } = projectInfo;
        const { name, pwd } = this.buildConfig.gitlab;
        this.log({ msg: `从仓库${repository_url}拉取代码`, showLine: false });
        const url = repository_url.replace('http://', '');

        await ctx.helper.sleep(100);
        // 只能这样嵌套了嘛。。
        await new Promise((resolve, reject) => {
          shell.exec(`git clone http://${name}:${pwd}@${url} .`, {}, (code, stdout, stderr) => {
            const logFileRowContent = JSON.stringify({ code, stdout, stderr });
            this.recordFileLog(logFileRowContent);
            // update project version form package.json
            // release/60a708c766f1a146b79d475d_1621560699290/package.json
            const packageJsonObj = require(`@root/${tempPath}/package.json`);
            if (packageJsonObj && packageJsonObj.hasOwnProperty('version')) {
              const currentVersion = packageJsonObj.version;
              projectInfo.version = currentVersion;
            }
            resolve(true);
          });
        });
        this.log('代码拉取成功...');

        this.log('开始安装依赖');
        await ctx.helper.sleep(100);
        // 3.安装依赖
        // shell.exec('yarn');
        await new Promise((resolve, reject) => {
          shell.exec('yarn', {}, (code, stdout, stderr) => {
            const logFileRowContent = JSON.stringify({ code, stdout, stderr });
            this.recordFileLog(logFileRowContent);
            resolve(true);
          });
        });

        this.log('依赖安装成功...');
        // 4.打包
        this.log('开始打包');
        await ctx.helper.sleep(100);
        // shell.exec('npm run build');
        await new Promise((resolve, reject) => {
          shell.exec('npm run build', {}, (code, stdout, stderr) => {
            const logFileRowContent = JSON.stringify({ code, stdout, stderr });
            this.recordFileLog(logFileRowContent);
            resolve(true);
          });
        });
        this.log('打包成功...');

        // 5.上传

        const { ftp_host, ftp_name, ftp_pwd, ftp_port } = projectInfo;
        if (ftp_host && ftp_name && ftp_pwd) {
          this.log('开始上传');
          // 这里的dist最好是可以配置,因为不同工程打包出来不一样。比如hbuilder打包出来的就不是unpackage/dist...
          await FtpService.up({ host: ftp_host, user: ftp_name, password: ftp_pwd, port: ftp_port, localRoot: `${tempPath}/dist` }).then(res => {
            this.log('上传成功...');
          }).catch(err => {
            this.log('上传失败:' + err.message);
          });
        }
        await ctx.helper.sleep(100);
        // 6.压缩备份


        // 更新数据
        const newProjectInfo = { ...projectInfo, build_count: Number(projectInfo.build_count) + 1 };
        await ctx.service.project.save(newProjectInfo);

        ctx.socket.emit('process', ctx.helper.parseMsg('process-success', { id }));
        const endTime = new Date().getTime();
        this.recordFileLog({ msg: '编译成功,耗时' + (endTime - startTime) / 1000 + '秒' });
        this.log('任务完成');

      } catch (e) {
        this.log('编译失败啦');
        this.recordFileLog({ msg: '编译失败' + e.message });
        const newProjectInfo = { ...projectInfo, err_count: Number(projectInfo.err_count) + 1 };
        await ctx.service.project.save(newProjectInfo);
        ctx.socket.emit('process', ctx.helper.parseMsg('process-fail', { id }));
      }
    }
  }

}

module.exports = ProcessController;

前端核心代码

{% extends "layout.html" %}

{% block right %}
<div class="action-head m-b-15">
    <a href="/project/form" type="button" class="btn btn-primary btn-sm">添加项目</a>
</div>
<div class="container-box">
    <table class="table fz-14">
        <thead>
        <tr>
            <th>序号</th>
            <th>名字</th>
            <th>描述</th>
            <th>仓库地址</th>
            <th>版本</th>
            <th>编译成功次数</th>
            <th>状态</th>
            <th>操作</th>
        </tr>
        </thead>
        <tbody>
        {% for project in projectList %}
        <tr>
            <th data-pid="{{project._id}}">{{loop.index}}</th>
            <td>{{project.title}}</td>
            <td>{{project.description}}</td>
            <td><a target="_blank" href="{{project.repository_url}}">{{project.repository_url}}</a></td>
            <td>{{project.version}}</td>
            <td><span id="build-count-{{project._id}}">{{project.build_count}}</span></td>
            <td>
                <div id="status-{{project._id}}">空闲</div>
            </td>
            <td style="white-space: nowrap;">
                <button onclick="projectRelease(this)" id="release-btn-{{project._id}}" data-id="{{project._id}}"
                        type="button" class="btn btn-primary btn-sm">
                    <i class="bi bi bi-cloud-upload m-r-4"></i>
                    一键发布
                </button>
                <button onclick="projectBuild(this)" data-id="{{project._id}}" type="button" class="btn btn-success btn-sm">
                    编译
                    一键发布
                </button>
                <a href="/project/form?id={{project.id}}" class="btn btn-info btn-sm m-l-10"><i
                        class="bi bi bi-gear m-r-4"></i>编辑配置</a>
                <button type="button" class="btn btn-danger btn-sm m-l-10"><i class="bi bi-back m-r-4"></i>回滚版本</button>
            </td>
        </tr>
        {% endfor %}
        </tbody>
    </table>
</div>


{% endblock %}

{% block script %}
<script>

  function addNotify({ msg, title = '提示', time = false }) {
    if (!document.getElementById('notifyBox')) {
      $('#wrap')
        .append(`<div id="notifyBox" style="position: fixed; top: 10px; right: 10px;z-index: 66"></div>`);
    }
    // <div id="notifyBox" style="position: fixed; top: 0; right: 0;z-index: 66"></div>
    if (!time) {
      time = dayjs()
        .format('YYYY-MM-DD HH:mm:ss');
    }
    const name = new Date().getTime();
    const tmplStr = `<div id="${name}" class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-delay="4000">
        <div class="toast-header">
            <i class="bi bi-info-circle m-r-4"></i>
            <strong class="mr-auto">${title}</strong>
            <small class="text-muted">${time}</small>
            <button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
        </div>
        <div class="toast-body">
            ${msg}
        </div>
    </div>`;
    $('#notifyBox')
      .append(tmplStr);
    $(`#${name}`)
      .toast('show');
    $(`#${name}`)
      .on('hidden.bs.toast', function() {
        $(`#${name}`)
          .remove();
      });
  }

  var socketReady = false

  $(function(){
    // if(!socketReady){
    //   socketCtx.open()
    //   socketReady = true
    // }
  })

  //别超时
  const socketCtx = io('/',{autoConnect:false});

  function ping() {
    console.log('ping')
    socketCtx.emit('ping', { time: new Date().getTime() });
    socketCtx.emit('chat', '123456789');
  }



  socketCtx.on('connect', () => {
    console.log('connect!',socketCtx.id);

    //不需要搞心跳
    // ping();
    // //心跳
    // setInterval(ping, 3* 1000);
    //socketCtx.emit('process', '123456789');
  });

  socketCtx.on("disconnect", () => {
    console.log('socket disconnect'); // undefined
  });

  socketCtx.on("connect_error", () => {
    console.log('connect_error emit')
    // socketCtx.connect();
  });

  socketCtx.on('log', res => {
    console.log('res from server:', JSON.stringify(res));
    const { action, payload } = res.data;
    const { id: projectId } = payload;
    const $currentProjectStatusEl = $(`#status-${projectId}`);
    const $currentProjectEl = $(`#release-btn-${projectId}`);
    switch (action) {
      case 'process-log':
        payload.toast && addNotify({ msg: payload.msg });
        payload.showLine && $currentProjectStatusEl.html(`${payload.msg}`);
        break;
      default:
        break;
    }
  })

  socketCtx.on('res', res => {
    if (typeof res === 'string') {
      console.log('res from server:', res);
      return;
    }
    console.log('res from server:', JSON.stringify(res));

  });


  socketCtx.on('process', res => {
    if (typeof res === 'string') {
      console.log('res from server:', res);
      return;
    }
    console.log('res from server:', JSON.stringify(res));
    const { action, payload } = res.data;
    const { id: projectId } = payload;
    const $currentProjectStatusEl = $(`#status-${projectId}`);
    const $currentProjectEl = $(`#release-btn-${projectId}`);
    switch (action) {
      case 'process-fail':
        // $currentProjectEl.html('一键发布')
        $currentProjectStatusEl.html(`<span class="color-red">发布失败</span>`);
        $currentProjectEl.attr('disabled', false);
        break;
      case 'process-success':
        $currentProjectStatusEl.html(`<span class="color-green">发布成功</span>`);
        // $currentProjectEl.html('一键发布')
        $currentProjectEl.attr('disabled', false);
        $(`#build-count-${projectId}`).html(Number($(`#build-count-${projectId}`).html())+1)
        break;
      default:
        break;
    }
  });

  async function projectBuild(el){
    const projectId = $(el).data('id');
    axios.post('/project/build',{projectId}).then(()=>{

    }).cache((err)=>{
      console.log(err)
    })
  }

  async function projectRelease(el) {
    if(!socketReady){
        socketCtx.open()
        socketReady = true
    }
    const projectId = $(el)
      .data('id');
    //alert(projectId)
    $(el)
      .attr('disabled', true);
    $(`#status-${projectId}`)
      .html('启动中...');
    const $currentProjectStatusEl = $(`#status-${projectId}`);
    $currentProjectStatusEl.prepend(`<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" id="${projectId}_loading"></span>`);
    socketCtx.emit('process', { id: projectId, action: 'release' });
    // await axios.post('/project/release',{id:projectId}).then(res=>{
    //   console.log(res)
    // }).catch(err=>{
    //   console.log(err)
    // }).finally(()=>{
    //
    // })
  }

</script>
{% endblock %}

错误截图

vue-cli的filenameHashing默认是true的,所以打包后的js应该是hash的

手动打包是正常的
手动打包
用了我的傻瓜工具
就好像吃了见手青一样看见了小人人

用这个代码,复现问题是必现的,会导致奇怪的问题。

'use strict';
const shell = require('shelljs');
shell.cd('release/vocen-engine-admin_1621566342725_60a72099ff8dfb0af9029188');
shell.exec('yarn');
shell.exec('npm run build');

问题分析

个人想法

1.第一层 首先编译后文件名不符合预期,就是webpack这里的配置加载有问题,一般自己手写就是这样
webpack.config.js

const path = require('path');
module.exports = {
  //就是output
  output: {
    filename: 'my-first-webpack.bundle.js',
    chunkFilename: (pathData) => {
      return pathData.chunk.name === 'main' ? '[name].js' : '[name]/[name].js';
    }
  },
};

2.第二层
而之前手撸webpack的过程中,一般都会在指令中或者配置文件去约定mode是production还是development。代码如下。

//package.json
"scripts": {
   "serve": "webpack-dev-server --mode development --progress --open",
   "build": "webpack --env.production --mode production --progress"
}

然后想到有可能是shelljs和真实的cmd差别,有可能就是他被设置成了development

开始捣鼓代码

然后就开始找filenamechunkFilename相关而配置.
不过一开始没有方向,只能从头开始把vue-cli代码看了一下,居然看了好久。虽然看高手代码很开心,但是还是有硬性的目标要弄完。在这个过程中,基本看了一下vue-cli的打包过程(dev的没有过多关注,只看了vue-cli-service build相关的逻辑)。
在读代码的过程中遗漏了一个重要代码,导致耽误了两个小时。

//node_modules/@vue/cli-service/lib/Service.js,
//概179行左右,自己找
const builtInPlugins = [
      './commands/serve',//我认真看了这个,下意识认为服务相关的都在这里了
      './commands/build',
      './commands/inspect',
      './commands/help',
		
      //我把后面的配置初始化这几个文件忽视了,失误太大了
      // config plugins are order sensitive
      './config/base',
      './config/css',
      './config/prod',
      './config/app'
    ].map(idToPlugin)

在这里插入图片描述
因为看错了文件,导致在commands/service上花费太多事件。去了解指令的注册、以及调用过程(这里面包含了好多次2个对照组的多次运行,然后控制台打印生成的webpack配置文件对比,花费不少时间)。
在没有溯源到output.filename相关配置时,我尝试用了vscode在node_modules目录中搜索关键字filename,结果vscode欺骗了我。。。没有结果。

然后又是继续愚公移山一样去走完该指令的所有过程,走完之后发现配置还是不一样,但是无法定位关键步骤在哪里。。。
这个时候想到是不是vscode忽略了node_modules中的代码,没有参与检索(我之前用webstrom等,搜索结果会忽略node_modules中的包,只展示业务代码的搜索结果),尝试着用了一下webstrom打开node_modules目录(不打开项目目录,直接打开node_modules作为根目录),一搜索filename,我发现好多符合结果的。。。一下子就觉得之前白干了,微软坑我。

然后开始快速浏览结果,浏览到一个app.js中有符合的结果,再一看,这个文件居然是在vue-cli包里面,福至心灵了。。。基本就确定尤大写的东西,还是可以层级很清晰而且是高度可以配置的,没必要去深究到webpack包那一层。

然后打开这文件好好看,看到这里基本就知道了。。。

 const isProd = process.env.NODE_ENV === 'production'
 const isLegacyBundle = process.env.VUE_CLI_MODERN_MODE && !process.env.VUE_CLI_MODERN_BUILD
 const outputDir = api.resolve(options.outputDir)

 const getAssetPath = require('../util/getAssetPath')
 const outputFilename = getAssetPath(
      options,
      `js/[name]${isLegacyBundle ? `-legacy` : ``}${isProd && options.filenameHashing ? '.[contenthash:8]' : ''}.js`
    )
 console.log('outputFilename is',outputFilename,isProd)
 webpackConfig
      .output
        .filename(outputFilename)
        .chunkFilename(outputFilename)

在控制台一打,确实如此。

 const isProd = process.env.NODE_ENV === 'production'
 console.log('isProd is',isProd)

傻瓜工具下运行 shell.exce(‘npm run build’)

outputFilename is false 

手动运行npm run build

outputFilename is true

分析为什么我绕了弯路

没有认真看代码。

node_modules/@vue/cli-service/lib/Service.js

在使用shelljs跑npm run build等指令(也就是vue-cli-service build时),Servie逻辑跑的时候时序很重要。

module.exports = class Service {
  constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
	//在这个resolvePlugins方法里面,配置了要引入的一堆东西。
	//关键里面的config/app.js
    this.plugins = this.resolvePlugins(plugins, useBuiltIn)
   
  }

 resolvePlugins (inlinePlugins, useBuiltIn) {
    const idToPlugin = id => ({
      id: id.replace(/^.\//, 'built-in:'),
      apply: require(id)
    })

    let plugins

    const builtInPlugins = [
      './commands/serve',
      './commands/build',
      './commands/inspect',
      './commands/help',
      // config plugins are order sensitive
      './config/base',
      './config/css',
      './config/prod',
      //这里面有用到process.env.NODE_ENV这个变量,来区分是dev还是prod.
      './config/app'
    ].map(idToPlugin)
 }

}
// node_modules/@vue/cli-service/lib/Service.js loadEnv方法 ,大概96行。
//我看到过如下代码。。
  loadEnv (mode) {
  	//省略一些代码。。。
  	
    // by default, NODE_ENV and BABEL_ENV are set to "development" unless mode
    // is production or test. However the value in .env files will take higher
    // priority.
    if (mode) {
      // always set NODE_ENV during tests
      // as that is necessary for tests to not be affected by each other
      const shouldForceDefaultEnv = (
        process.env.VUE_CLI_TEST &&
        !process.env.VUE_CLI_TEST_TESTING_ENV
      )

      const defaultNodeEnv = (mode === 'production' || mode === 'test')
        ? mode
        : 'development'
      if (shouldForceDefaultEnv || process.env.NODE_ENV == null) {
        process.env.NODE_ENV = defaultNodeEnv
      }
      if (shouldForceDefaultEnv || process.env.BABEL_ENV == null) {
        process.env.BABEL_ENV = defaultNodeEnv
      }
    }
    //省略一些代码。。。
  }

原因记录

@vue\cli-service\lib\config\app.js

const isProd = process.env.NODE_ENV === 'production'
/*
do something
*/
const outputFilename = getAssetPath(
      options,
      `js/[name]${isLegacyBundle ? `-legacy` : ``}${isProd && options.filenameHashing ? '.[contenthash:8]' : ''}.js`
    )

具体位置在22行左右(版本不一致,自己找下)
在这里插入图片描述
就是这个错误

shelljs和cmd运行有什么差别呢??

又去看了shelljs的包介绍。
在这里插入图片描述
英文看不太懂,然后看源码

  //node_modules/shelljs/src/exec.js 29行
  opts = common.extend({
    silent: common.config.silent,
    cwd: _pwd().toString(),
    env: process.env,
    maxBuffer: DEFAULT_MAXBUFFER_SIZE,
    encoding: 'utf8',
  }, opts);

解决办法


shell.exec('npm run build', { env: Object.assign({}, process.env, { NODE_ENV: 'production' }) });

在这里插入图片描述

经验总结

仔细看文档,多看源码,善用工具。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值