玩转编程语言:构建自定义代码生成器

在真实的软件开发过程中,无论使用何种编程开发语言,都不可避免的会遇到代码重复的问题。如何处理重复的问题,可以选择情怀(手动再敲一遍),也可以选择 Copy-to-Copy ,或者选择代码生成器。正如在之前的文章 我的写作工具链 中,我介绍过一种 Blog 生成器 hexo ,可以将 Markdown 格式的内容自动生成方便发布的 HTML 格式。本文将还原 hexo 的运行原理,为解决类似问题提供一些参考思路。

示例:通过 Markdown 文件声明模板(源代码),通过脚本生成 HTML 文件(目标代码),并预览代码生成效果。

Step 1: 准备环境 (dependencies)

开发语言 Node.js, 一个能够运行 JavaScript 的开放源代码、跨平台运行环境。

  • npm init — 初始化 root 目录
  • npm i -s live-server — 该模块支持本示例生成静态 HTML 站点,提供热部署能力
  • npm i -s nodemon — 该模块支持当文件变化自动执行重构任务
  • npm i -s concurrently — 该模块支持支持并发执行任务、脚本(scripts/tasks)
  • npm i -s markdown-it — 该模块提供 Markdown 文件解析器
mkdir project-generator
mkdir project-generator/pages
mkdir project-generator/pages_meta
mkdir project-generator/js
mkdir project-generator/css
mkdir project-generator/images
mkdir project-generator/build_scripts
mkdir project-generator/build

cd project-generator
npm init 
npm i -s concurrently
npm i -s fs
npm i -s fs-extra
npm i -s markdown-it
npm i -s live-server 
npm i -s nodemon

[图片上传失败...(image-7d14f7-1514002237128)]

Step 2: 准备元数据

例如:pages/index.md

# Home page

Hello world!

[Link to another page](./other.html)

例如:pages_meta/index.json 用于存储一些需要的元数据(参数、固定内容等),JSON 文件格式方便后面调用。

{
  "lang": "en",
  "title": "Index",
  "stylesheets": ["./css/style.css"],
  "scripts": ["./js/main.js"],
  "charset": "utf-8",
  "description": "This is a page",
  "keywords": "page, sample",
  "author": "None",
  "favicon": "./images/favicon.png",
  "viewport": "width=device-width, initial-scale=1",
  "extra": []
}

Step 3: 编写模板和构建脚本(template & build Script)

代码生成器中需要定制开发的部分包括 builder.jspages_template.js。build.js 相当于 main 函数,控制入口和流程,加载资源数据、调用 generator 任务,与 Makefile 和 Ant.xml 非常类似。pages_template.js 依赖的组件是 markdown-it ,负责将 Markdown 源文件转换输出成 HTML 文件。builder.js 将 pages_template.js 视为一个模块引用:pageTemplate.generatePage(pageContent, metaData)) 因此可以根据需要定制多个不同的 XXX_template.js 或者在每个 template.js 中定义其它函数。

builder.js

var pageTemplate = require('./page_template');

// All paths are relative to package.json.
var pagesPath = './pages';
var pagesMetaPath = './pages_meta';
var copyFolders = ['./images', './css', './js'];
var outputPath = './build';

// Clean
console.log('Cleaning previous build...');
for (var file of fs.readdirSync(outputPath)){
    fs.removeSync(path.join(outputPath, file));
}

//Loading
console.log('Loading pages metadata...');
  for(var pageMeta of fs.readdirSync(pagesMetaPath)){
    pagesMeta[pageMeta] = fs.readFileSync(path.join(pagesMetaPath,pageMeta),'utf8');
  }
}

// Generate
console.log('Generating pages...');
for(var page of Object.entries(pages)) {
    var pageName = page[0].slice(0, page[0].lastIndexOf('.'));
    var metaData = pagesMeta.hasOwnProperty(pageName+'.json')
      ? JSON.parse(pagesMeta[pageName+'.json'])
      : {};
    metaData.title = metaData.title || pageName;
    var pageContent = page[1];
    fs.writeFileSync(
      path.join(outputPath,pageName+'.html'),
      pageTemplate.generatePage(pageContent, metaData));
  }
}

// Copy
console.log('Copying folders...');
  for(var copyFolder of copyFolders){
    fs.copySync(copyFolder, path.join(outputPath,copyFolder));
  }

pages_template.js

var md = require('markdown-it')();

module.exports = {
  generatePage: function(pageContent,pageMeta){
    return`<!DOCTYPE html>
<html lang="${pageMeta.lang || this.defaultMeta.lang}">
  <head>
    <title>${pageMeta.title || this.defaultMeta.title}</title>
    <meta charset="${pageMeta.charset || this.defaultMeta.charset}">
    <meta name="description" content="${pageMeta.description || this.defaultMeta.description}">
    <meta name="keywords" content="${pageMeta.keywords || this.defaultMeta.keywords}">
    <meta name="author" content="${pageMeta.author || this.defaultMeta.author}">
</head>
<body>
  ${md.render(pageContent)}
</body>
</html>
    `;
  }
}

Step 4: 优化任务脚本

在 Step 1 步骤中,npm init 创建了一个文件:package.json,我们可以定义其中的 “scripts” , 执行 npm run start 将默认在 1080 端口开启 Web 服务。

[图片上传失败...(image-6b1322-1514002237128)]

{
  "name": "coffee",
  "version": "1.0.0",
  "description": "beyond my coffee",
  "main": "index.js",
  "scripts": {  
    "build-pages": "node ./build_scripts/builder.js",
    "start": "concurrently --kill-others \"nodemon -e js,json,css,md -i build -x \\\"npm run build-pages\\\"\" \"live-server ./build\""
  },
  "author": "@RiboseYim"
}
$ npm run build-pages

> coffee@1.0.0 build-pages /generator-code
> node ./build_scripts/builder.js

Cleaning previous build...
Loading pages...
Loading pages metadata...
Generating pages...
Copying folders...
Done!

$ npm run start

> coffee@1.0.0 start /Users/yanrui/project/generator-code
> concurrently --kill-others "nodemon -e js,json,css,md -i build -x \"npm run build-pages\"" "live-server ./build"

[0] [nodemon] 1.14.1
[0] [nodemon] to restart at any time, enter `rs`
[0] [nodemon] watching: *.*
[0] [nodemon] starting `npm run build-pages`
[1] Serving "./build" at http://127.0.0.1:8080
[1] Ready for changes
[1] GET /js/main.js 404 42.133 ms - 23
[1] GET /js/main.js 404 12.204 ms - 23
[0]
[0] > coffee@1.0.0 build-pages /Users/yanrui/project/generator-code
[0] > node ./build_scripts/builder.js
[0]
[0] Cleaning previous build...
[0] Loading pages...
[0] Loading pages metadata...
[0] Generating pages...
[0] Copying folders...
[0] Done!
[0] [nodemon] clean exit - waiting for changes before restart
[1] Change detected build/index.html
[1] Change detected build/images
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 什么是 NPM npm之于Node,就像pip之于Python,gem之于Ruby,composer之于PHP。 ...
    ihoey阅读 6,203评论 2 36
  • 无意中看到zhangwnag大佬分享的webpack教程感觉受益匪浅,特此分享以备自己日后查看,也希望更多的人看到...
    小小字符阅读 8,084评论 7 35
  • 高中两年的陪伴,你的细心呵护,让我产生了一种错觉,我以为,你是爱我的。 高中二年级的时候,你跟随大家出去...
    安雨薇阅读 334评论 0 0
  • 周六,我和彭泽在公司值班。百无聊赖之际,他开启打印机,A4纸缓缓出来的沙沙声打破了办公室的宁静。我问他在打印什么?...
    余想阅读 324评论 3 3