页面生成器之数据如何和模板进行结合生成最终页面

360 阅读2分钟

github.com/roooses/ros…

框架

egg.js

核心代码

const Service = require('egg').Service;
const process = require('child_process');
const Metalsmith = require('metalsmith');
const Handlebars = require('handlebars')
const download = require('download-git-repo')
const path = require('path')
const fs = require('fs')
const utils = require('./fileUtils');


function resolveData(data) {
  data.noRepeatCpsName = [];
  data.outJsList = [];
  for (let item of data.components) {
    if (data.noRepeatCpsName.indexOf(item.name) < 0) {
      data.noRepeatCpsName.push(item.name);
    }
    for (let jsItem of item.outJs) {
      if (data.outJsList.indexOf(jsItem) < 0) {
        data.outJsList.push(jsItem);
      }
    }
  }
  return data
}

function build(data, temp_dest, source, dest, cb, ignore) {
  if (data.length) {
    let result = {
      repoName: data.repoName,
      version: data.version
    }
    data.forEach((pageConfig, index) => {
      result[`page${index + 1}`] = resolveData(pageConfig)
    })
    data = result
  } else {
    data = resolveData(data)
  }
  let metalsmith = Metalsmith(temp_dest)
    .use(renderTemplateFiles(data))
    .source(source)
    .destination(dest)
    .clean(false)

  if (ignore) {
    metalsmith.ignore(filePath => {
      filePath = filePath.replace(path.join(temp_dest, source), '')
      filePath = path.join(dest, filePath)
      return fs.existsSync(filePath)
    })
  }
  return metalsmith.build((error, files) => {
    if (error) console.log(error);
    let f = Object.keys(files)
      .filter(o => fs.existsSync(path.join(dest, o)))
      .map(o => path.join(dest, o))
    cb(error, f)
  })
}

function renderTemplateFiles(data) {
  return function (files) {
    Object.keys(files).forEach((fileName) => {
      console.log(fileName)
      let file = files[fileName]
      // 渲染方法
      file.contents = Handlebars.compile(file.contents.toString())(data)
    })
  }
}

function downloadFunc(downloadRepoUrl, temp_dest) {
  return new Promise(async (resolve, reject) => {
    console.log(downloadRepoUrl);
    download(downloadRepoUrl, temp_dest, {clone: true}, (err) => {
      if (err) {
        console.log(err);
        reject('请求模板下载失败');
      } else {
        resolve('请求模板下载成功');
      }
    })
  });
}

function buildFunc(req, temp_dest) {
  return new Promise(async (resolve, reject) => {
    req.templateConfig.port = await utils.getPort();
    if (req.templateConfig.port) {
      build(req.templateConfig, temp_dest, 'template', req.templateConfig.repoName, (err, f) => {
        if (err) {
          reject(err);
        } else {
          console.log('to repoName完成')
          resolve(req.templateConfig.port);
        }
      })
    } else {
      reject('未正确获取到port');
    }
  });
}
class TplService extends Service {
  async renderTpl(req, tplDetail) {
    return new Promise(async (resolve, reject) => {
      Handlebars.registerHelper('upcasefirst', function (value) {
        let str = '';
        let arr = value.split('-');
        for (let item of arr) {
          str += item[0].toUpperCase() + item.slice(1);
        }
        return str;
      });

      Handlebars.registerHelper('tostring', function (value) {
        return JSON.stringify(value);
      });

      Handlebars.registerHelper('parse', function (value) {
        let result = '';
        Object.keys(value).forEach((key) => {
          result += `\:${key}\=\'${value[key]}\' `
        });
        return result;
      });
      if (!(await utils.existOrNot('./static'))) {
        await utils.mkdirFolder('static');
      }
      // 基础模版所在目录
      const temp_dest = `static/tmp${req.templateId}`
      // 删除本地该仓库内容
      process.exec(`cd static && rm -rf tmp${req.templateId}/${req.templateConfig.repoName}`, async function (error, stdout, stderr) {
        if (error) {
          reject(error);
        } else {
          let hasExist = await utils.existOrNot(`./static/tmp${req.templateId}`);
          if (!hasExist) {
            console.log('temp_dest:', temp_dest)
            let downloadRes = await downloadFunc(req.templateConfig.repoUrl, temp_dest);
          }
          let port = await buildFunc(req, temp_dest);
          resolve(port);
        }

      })
    })
  }
}

module.exports = TplService;

代码解析

当后台提交渲染请求的时候,我们的 node 服务所做的工作主要是:

  1. 拉取对应模板

  2. 渲染数据

  3. 编译

拉取也就是去指定仓库中通过download-git-repo插件进行拉取模板。编译其实也就是通过metalsmith静态模板生成器把模板作为输入,数据作为填充,按照handlebars的语法进行规则渲染。最后产出build构建好的目录。在这一步,我们之前所需的组件,会被渲染进package.json文件。我们来看一下核心代码:

// 这里就像一个管道,以数据入口为生成源,通过renderTemplateFiles编译产出到目标目录
function build(data, temp_dest, source, dest, cb) {
  let metalsmith = Metalsmith(temp_dest)
    .use(renderTemplateFiles(data))
    .source(source)
    .destination(dest)
    .clean(false)

  return metalsmith.build((error, files) => {
    if (error) console.log(error);
    let f = Object.keys(files)
      .filter(o => fs.existsSync(path.join(dest, o)))
      .map(o => path.join(dest, o))
    cb(error, f)
  })
}


function renderTemplateFiles(data) {
  return function (files) {
    Object.keys(files).forEach((fileName) => {
      let file = files[fileName]
      // 渲染方法
      file.contents = Handlebars.compile(file.contents.toString())(data)
    })
  }
}

最后我们得到的是一个 Vue 项目,此时还不能直接跑在浏览器端,这里就涉及到当前发布系统所支持的形式了。怎么说?如果你的公司发布系统需要在线编译,那么你可以把源文件直接上传到git仓库,触发仓库的 WebHook 让发布系统替你发掉这个项目即可。如果你们的发布系统是需要你编译后提交编译文件进行发布的,那么你可以通过 node 命令,进行本地构建,产出 HTML,CSS,JS。直接提交给发布系统即可。

发布

最后我们得到的是一个 Vue 项目,项目文件在rose-server 同目录的port下,此时还不能直接跑在浏览器端,这里就涉及到当前发布系统所支持的形式了

如果你的公司发布系统需要在线编译,那么你可以把源文件直接上传到git仓库,触发仓库的 WebHook 让发布系统替你发掉这个项目即可

如果你们的发布系统是需要你编译后提交编译文件进行发布的,那么你可以通过 node 命令,进行本地构建,产出 HTML,CSS,JS