一个小贝贝 發表於 2019-5-10 08:51:00

使用 Node.js 写一个代码生成器

<h2 id="背景">背景</h2>
<p>第一次接触代码生成器用的是动软代码生成器,数据库设计好之后,一键生成后端 curd代码。之后也用过 CodeSmith , T4。目前市面上也有很多优秀的代码生成器,而且大部分都提供可视化界面操作。</p>
<p>自己写一个的原因是因为要集成到自己写的一个小工具中,而且使用 Node.js 这种动态脚本语言进行编写更加灵活。</p>
<h2 id="原理">原理</h2>
<p>代码生成器的原理就是:<code>数据 + 模板 =&gt; 文件</code>。</p>
<p><code>数据</code>一般为数据库的表字段结构。</p>
<p><code>模板</code>的语法与使用的模板引擎有关。</p>
<p>使用模板引擎将<code>数据</code>和<code>模板</code>进行编译,编译后的内容输出到文件中就得到了一份代码文件。</p>
<h2 id="功能">功能</h2>
<p>因为这个代码生成器是要集成到一个小工具 lazy-mock 内,这个工具的主要功能是启动一个 mock server 服务,包含curd功能,并且支持数据的持久化,文件变化的时候自动重启服务以最新的代码提供 api mock 服务。</p>
<p>代码生成器的功能就是根据配置的数据和模板,编译后将内容输出到指定的目录文件中。因为添加了新的文件,mock server 服务会自动重启。</p>
<p>还要支持模板的定制与开发,以及使用 CLI 安装模板。</p>
<p>可以开发前端项目的模板,直接将编译后的内容输出到前端项目的相关目录下,webpack 的热更新功能也会起作用。</p>
<h2 id="模板引擎">模板引擎</h2>
<p>模板引擎使用的是 nunjucks。</p>
<p>lazy-mock 使用的构建工具是 gulp,使用 gulp-nodemon 实现 mock-server 服务的自动重启。所以这里使用 gulp-nunjucks-render 配合 gulp 的构建流程。</p>
<h2 id="代码生成">代码生成</h2>
<p>编写一个 gulp task :</p>
<pre><code class="language-js">const rename = require('gulp-rename')
const nunjucksRender = require('gulp-nunjucks-render')
const codeGenerate = require('./templates/generate')
const ServerFullPath = require('./package.json').ServerFullPath; //mock -server项目的绝对路径
const FrontendFullPath = require('./package.json').FrontendFullPath; //前端项目的绝对路径
const nunjucksRenderConfig = {
path: 'templates/server',
envOptions: {
    tags: {
      blockStart: '&lt;%',
      blockEnd: '%&gt;',
      variableStart: '&lt;$',
      variableEnd: '$&gt;',
      commentStart: '&lt;#',
      commentEnd: '#&gt;'
    },
},
ext: '.js',
//以上是 nunjucks 的配置
ServerFullPath,
FrontendFullPath
}
gulp.task('code', function () {
require('events').EventEmitter.defaultMaxListeners = 0
return codeGenerate(gulp, nunjucksRender, rename, nunjucksRenderConfig)
});
</code></pre>
<blockquote>
<p>代码具体结构细节可以打开 lazy-mock 进行参照</p>
</blockquote>
<p>为了支持模板的开发,以及更灵活的配置,我将代码生成的逻辑全都放在模板目录中。</p>
<p><code>templates</code> 是存放模板以及数据配置的目录。结构如下:</p>
<p><img src="https://user-gold-cdn.xitu.io/2019/5/10/16a9e71ad9b2e8b1?w=433&amp;h=660&amp;f=png&amp;s=27198" alt="" loading="lazy"></p>
<p>只生成 lazy-mock 代码的模板中 :</p>
<p><code>generate.js</code>的内容如下:</p>
<pre><code class="language-js">const path = require('path')
const CodeGenerateConfig = require('./config').default;
const Model = CodeGenerateConfig.model;

module.exports = function generate(gulp, nunjucksRender, rename, nunjucksRenderConfig) {
    nunjucksRenderConfig.data = {
      model: CodeGenerateConfig.model,
      config: CodeGenerateConfig.config
    }
    const ServerProjectRootPath = nunjucksRenderConfig.ServerFullPath;
    //server
    const serverTemplatePath = 'templates/server/'
    gulp.src(`${serverTemplatePath}controller.njk`)
      .pipe(nunjucksRender(nunjucksRenderConfig))
      .pipe(rename(Model.name + '.js'))
      .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ControllerRelativePath));

    gulp.src(`${serverTemplatePath}service.njk`)
      .pipe(nunjucksRender(nunjucksRenderConfig))
      .pipe(rename(Model.name + 'Service.js'))
      .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ServiceRelativePath));

    gulp.src(`${serverTemplatePath}model.njk`)
      .pipe(nunjucksRender(nunjucksRenderConfig))
      .pipe(rename(Model.name + 'Model.js'))
      .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ModelRelativePath));

    gulp.src(`${serverTemplatePath}db.njk`)
      .pipe(nunjucksRender(nunjucksRenderConfig))
      .pipe(rename(Model.name + '_db.json'))
      .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.DBRelativePath));

    return gulp.src(`${serverTemplatePath}route.njk`)
      .pipe(nunjucksRender(nunjucksRenderConfig))
      .pipe(rename(Model.name + 'Route.js'))
      .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.RouteRelativePath));
}
</code></pre>
<p>类似:</p>
<pre><code class="language-js">gulp.src(`${serverTemplatePath}controller.njk`)
      .pipe(nunjucksRender(nunjucksRenderConfig))
      .pipe(rename(Model.name + '.js'))
      .pipe(gulp.dest(ServerProjectRootPath + CodeGenerateConfig.config.ControllerRelativePath));
</code></pre>
<p>表示使用 controller.njk 作为模板,nunjucksRenderConfig作为数据(模板内可以获取到 nunjucksRenderConfig 属性 data 上的数据)。编译后进行文件重命名,并保存到指定目录下。</p>
<p><code>model.js</code> 的内容如下:</p>
<pre><code class="language-js">var shortid = require('shortid')
var Mock = require('mockjs')
var Random = Mock.Random

//必须包含字段id
export default {
    name: "book",
    Name: "Book",
    properties: [
      {
            key: "id",
            title: "id"
      },
      {
            key: "name",
            title: "书名"
      },
      {
            key: "author",
            title: "作者"
      },
      {
            key: "press",
            title: "出版社"
      }
    ],
    buildMockData: function () {//不需要生成设为false
      let data = []
      for (let i = 0; i &lt; 100; i++) {
            data.push({
                id: shortid.generate(),
                name: Random.cword(5, 7),
                author: Random.cname(),
                press: Random.cword(5, 7)
            })
      }
      return data
    }
}
</code></pre>
<p>模板中使用最多的就是这个数据,也是生成新代码需要配置的地方,比如这里配置的是 book ,生成的就是关于 book 的curd 的 mock 服务。要生成别的,修改后执行生成命令即可。</p>
<p>buildMockData 函数的作用是生成 mock 服务需要的随机数据,在 db.njk 模板中会使用:</p>
<pre><code>{
"&lt;$ model.name $&gt;":&lt;% if model.buildMockData %&gt;&lt;$ model.buildMockData()|dump|safe $&gt;&lt;% else %&gt;[]&lt;% endif %&gt;
}
</code></pre>
<blockquote>
<p>这也是 nunjucks 如何在模板中执行函数</p>
</blockquote>
<p><code>config.js</code> 的内容如下:</p>
<pre><code class="language-js">export default {
    //server
    RouteRelativePath: '/src/routes/',
    ControllerRelativePath: '/src/controllers/',
    ServiceRelativePath: '/src/services/',
    ModelRelativePath: '/src/models/',
    DBRelativePath: '/src/db/'
}
</code></pre>
<p>配置相应的模板编译后保存的位置。</p>
<p><code>config/index.js</code> 的内容如下:</p>
<pre><code class="language-js">import model from './model';
import config from './config';
export default {
    model,
    config
}
</code></pre>
<p>针对 lazy-mock 的代码生成的功能就已经完成了,要实现模板的定制直接修改模板文件即可,比如要修改 mock server 服务 api 的接口定义,直接修改 route.njk 文件:</p>
<pre><code class="language-js">import KoaRouter from 'koa-router'
import controllers from '../controllers/index.js'
import PermissionCheck from '../middleware/PermissionCheck'

const router = new KoaRouter()
router
    .get('/&lt;$ model.name $&gt;/paged', controllers.&lt;$model.name $&gt;.get&lt;$ model.Name $&gt;PagedList)
    .get('/&lt;$ model.name $&gt;/:id', controllers.&lt;$ model.name $&gt;.get&lt;$ model.Name $&gt;)
    .del('/&lt;$ model.name $&gt;/del', controllers.&lt;$ model.name $&gt;.del&lt;$ model.Name $&gt;)
    .del('/&lt;$ model.name $&gt;/batchdel', controllers.&lt;$ model.name $&gt;.del&lt;$ model.Name $&gt;s)
    .post('/&lt;$ model.name $&gt;/save', controllers.&lt;$ model.name $&gt;.save&lt;$ model.Name $&gt;)

module.exports = router
</code></pre>
<h2 id="模板开发与安装">模板开发与安装</h2>
<p>不同的项目,代码结构是不一样的,每次直接修改模板文件会很麻烦。</p>
<p>需要提供这样的功能:针对不同的项目开发一套独立的模板,支持模板的安装。</p>
<p>代码生成的相关逻辑都在模板目录的文件中,模板开发没有什么规则限制,只要保证目录名为 <code>templates</code>,<code>generate.js</code>中导出<code>generate</code>函数即可。</p>
<p>模板的安装原理就是将模板目录中的文件全部覆盖掉即可。不过具体的安装分为本地安装与在线安装。</p>
<p>之前已经说了,这个代码生成器是集成在 lazy-mock 中的,我的做法是在初始化一个新 lazy-mock 项目的时候,指定使用相应的模板进行初始化,也就是安装相应的模板。</p>
<p>使用 Node.js 写了一个 CLI 工具 lazy-mock-cli,已发到 npm ,其功能包含下载指定的远程模板来初始化新的 lazy-mock 项目。代码参考( copy )了 vue-cli2。代码不难,说下某些关键点。</p>
<p>安装 CLI 工具:</p>
<pre><code class="language-bash">npm install lazy-mock -g
</code></pre>
<p>使用模板初始化项目:</p>
<pre><code class="language-bash">lazy-mock init d2-admin-pm my-project
</code></pre>
<blockquote>
<p>d2-admin-pm 是我为一个前端项目已经写好的一个模板。</p>
</blockquote>
<p><code>init</code> 命令调用的是 lazy-mock-init.js 中的逻辑:</p>
<pre><code class="language-js">#!/usr/bin/env node
const download = require('download-git-repo')
const program = require('commander')
const ora = require('ora')
const exists = require('fs').existsSync
const rm = require('rimraf').sync
const path = require('path')
const chalk = require('chalk')
const inquirer = require('inquirer')
const home = require('user-home')
const fse = require('fs-extra')
const tildify = require('tildify')
const cliSpinners = require('cli-spinners');
const logger = require('../lib/logger')
const localPath = require('../lib/local-path')

const isLocalPath = localPath.isLocalPath
const getTemplatePath = localPath.getTemplatePath

program.usage('&lt;template-name&gt; ')
    .option('-c, --clone', 'use git clone')
    .option('--offline', 'use cached template')

program.on('--help', () =&gt; {
    console.log('Examples:')
    console.log()
    console.log(chalk.gray('    # create a new project with an official template'))
    console.log('    $ lazy-mock init d2-admin-pm my-project')
    console.log()
    console.log(chalk.gray('    # create a new project straight from a github template'))
    console.log('    $ vue init username/repo my-project')
    console.log()
})

function help() {
    program.parse(process.argv)
    if (program.args.length &lt; 1) return program.help()
}
help()
//模板
let template = program.args
//判断是否使用官方模板
const hasSlash = template.indexOf('/') &gt; -1
//项目名称
const rawName = program.args
//在当前文件下创建
const inPlace = !rawName || rawName === '.'
//项目名称
const name = inPlace ? path.relative('../', process.cwd()) : rawName
//创建项目完整目标位置
const to = path.resolve(rawName || '.')
const clone = program.clone || false

//缓存位置
const serverTmp = path.join(home, '.lazy-mock', 'sever')
const tmp = path.join(home, '.lazy-mock', 'templates', template.replace(/[\/:]/g, '-'))
if (program.offline) {
    console.log(`&gt; Use cached template at ${chalk.yellow(tildify(tmp))}`)
    template = tmp
}

//判断是否当前目录下初始化或者覆盖已有目录
if (inPlace || exists(to)) {
    inquirer.prompt([{
      type: 'confirm',
      message: inPlace
            ? 'Generate project in current directory?'
            : 'Target directory exists. Continue?',
      name: 'ok'
    }]).then(answers =&gt; {
      if (answers.ok) {
            run()
      }
    }).catch(logger.fatal)
} else {
    run()
}

function run() {
    //使用本地缓存
    if (isLocalPath(template)) {
      const templatePath = getTemplatePath(template)
      if (exists(templatePath)) {
            generate(name, templatePath, to, err =&gt; {
                if (err) logger.fatal(err)
                console.log()
                logger.success('Generated "%s"', name)
            })
      } else {
            logger.fatal('Local template "%s" not found.', template)
      }
    } else {
      if (!hasSlash) {
            //使用官方模板
            const officialTemplate = 'lazy-mock-templates/' + template
            downloadAndGenerate(officialTemplate)
      } else {
            downloadAndGenerate(template)
      }
    }
}

function downloadAndGenerate(template) {
    downloadServer(() =&gt; {
      downloadTemplate(template)
    })
}

function downloadServer(done) {
    const spinner = ora('downloading server')
    spinner.spinner = cliSpinners.bouncingBall
    spinner.start()
    if (exists(serverTmp)) rm(serverTmp)
    download('wjkang/lazy-mock', serverTmp, { clone }, err =&gt; {
      spinner.stop()
      if (err) logger.fatal('Failed to download server ' + template + ': ' + err.message.trim())
      done()
    })
}

function downloadTemplate(template) {
    const spinner = ora('downloading template')
    spinner.spinner = cliSpinners.bouncingBall
    spinner.start()
    if (exists(tmp)) rm(tmp)
    download(template, tmp, { clone }, err =&gt; {
      spinner.stop()
      if (err) logger.fatal('Failed to download template ' + template + ': ' + err.message.trim())
      generate(name, tmp, to, err =&gt; {
            if (err) logger.fatal(err)
            console.log()
            logger.success('Generated "%s"', name)
      })
    })
}

function generate(name, src, dest, done) {
    try {
      fse.removeSync(path.join(serverTmp, 'templates'))
      const packageObj = fse.readJsonSync(path.join(serverTmp, 'package.json'))
      packageObj.name = name
      packageObj.author = ""
      packageObj.description = ""
      packageObj.ServerFullPath = path.join(dest)
      packageObj.FrontendFullPath = path.join(dest, "front-page")
      fse.writeJsonSync(path.join(serverTmp, 'package.json'), packageObj, { spaces: 2 })
      fse.copySync(serverTmp, dest)
      fse.copySync(path.join(src, 'templates'), path.join(dest, 'templates'))
    } catch (err) {
      done(err)
      return
    }
    done()
}
</code></pre>
<p>判断了是使用本地缓存的模板还是拉取最新的模板,拉取线上模板时是从官方仓库拉取还是从别的仓库拉取。</p>
<h2 id="一些小问题">一些小问题</h2>
<p>目前代码生成的相关数据并不是来源于数据库,而是在 <code>model.js</code> 中简单配置的,原因是我认为一个 mock server 不需要数据库,lazy-mock 确实如此。</p>
<p>但是如果写一个正儿八经的代码生成器,那肯定是需要根据已经设计好的数据库表来生成代码的。那么就需要连接数据库,读取数据表的字段信息,比如字段名称,字段类型,字段描述等。而不同关系型数据库,读取表字段信息的 sql 是不一样的,所以还要写一堆balabala的判断。可以使用现成的工具 sequelize-auto , 把它读取的 model 数据转成我们需要的格式即可。</p>
<p>生成前端项目代码的时候,会遇到这种情况:</p>
<p>某个目录结构是这样的:</p>
<p><img src="https://user-gold-cdn.xitu.io/2019/5/10/16a9ef45e3963c64?w=660&amp;h=380&amp;f=png&amp;s=14236" alt="" loading="lazy"></p>
<p><code>index.js</code> 的内容:</p>
<pre><code class="language-js">import layoutHeaderAside from '@/layout/header-aside'
export default {
    "layoutHeaderAside": layoutHeaderAside,
    "menu": () =&gt; import(/* webpackChunkName: "menu" */'@/pages/sys/menu'),
    "route": () =&gt; import(/* webpackChunkName: "route" */'@/pages/sys/route'),
    "role": () =&gt; import(/* webpackChunkName: "role" */'@/pages/sys/role'),
    "user": () =&gt; import(/* webpackChunkName: "user" */'@/pages/sys/user'),
    "interface": () =&gt; import(/* webpackChunkName: "interface" */'@/pages/sys/interface')
}
</code></pre>
<p>如果添加一个 book 就需要在这里加上<code>"book": () =&gt; import(/* webpackChunkName: "book" */'@/pages/sys/book')</code></p>
<p>这一行内容也是可以通过配置模板来生成的,比如模板内容为:</p>
<pre><code>"&lt;$ model.name $&gt;": () =&gt; import(/* webpackChunkName: "&lt;$ model.name $&gt;" */'@/pages&lt;$ model.module $&gt;&lt;$ model.name $&gt;')
</code></pre>
<p>但是生成的内容怎么加到<code>index.js</code>中呢?</p>
<p>第一种方法:复制粘贴</p>
<p>第二种方法:</p>
<p>这部分的模板为 routerMapComponent.njk :</p>
<pre><code>export default {
    "&lt;$ model.name $&gt;": () =&gt; import(/* webpackChunkName: "&lt;$ model.name $&gt;" */'@/pages&lt;$ model.module $&gt;&lt;$ model.name $&gt;')
}
</code></pre>
<p>编译后文件保存到 routerMapComponents 目录下,比如 book.js</p>
<p>修改 index.js :</p>
<pre><code class="language-js">const files = require.context('./', true, /\.js$/);
import layoutHeaderAside from '@/layout/header-aside'

let componentMaps = {
    "layoutHeaderAside": layoutHeaderAside,
    "menu": () =&gt; import(/* webpackChunkName: "menu" */'@/pages/sys/menu'),
    "route": () =&gt; import(/* webpackChunkName: "route" */'@/pages/sys/route'),
    "role": () =&gt; import(/* webpackChunkName: "role" */'@/pages/sys/role'),
    "user": () =&gt; import(/* webpackChunkName: "user" */'@/pages/sys/user'),
    "interface": () =&gt; import(/* webpackChunkName: "interface" */'@/pages/sys/interface'),
}
files.keys().forEach((key) =&gt; {
    if (key === './index.js') return
    Object.assign(componentMaps, files(key).default)
})
export default componentMaps
</code></pre>
<blockquote>
<p>使用了 require.context</p>
</blockquote>
<p>我目前也是使用了这种方法</p>
<p>第三种方法:</p>
<p>开发模板的时候,做特殊处理,读取原有 index.js 的内容,按行进行分割,在数组的最后一个元素之前插入新生成的内容,注意逗号的处理,将新数组内容重新写入 index.js 中,注意换行。</p>
<h2 id="打个广告">打个广告</h2>
<p>如果你想要快速的创建一个 mock-server,同时还支持数据的持久化,又不需要安装数据库,还支持代码生成器的模板开发,欢迎试试 lazy-mock 。</p><br><br>
来源:https://www.cnblogs.com/jaycewu/p/10842426.html
頁: [1]
查看完整版本: 使用 Node.js 写一个代码生成器