AStaticDocumentationGeneratorin40LinesofCode前言原文:用Node.js构建一个静态站点生成器40行作者:DouglasMatoso译者:SimonMa日期:2017-09-14为什么要造这个轮子当我打算建立一个个人网站,我的需求很简单,做一个网站,就几个页面,放一些自己的资料,自己的技能和项目就够了。毫无疑问,它应该是纯静态的(不需要后端服务,可以在任何地方托管)。我用过Jekyll、Hugo和Hexo,这些都是著名的静态文档生成器,但我认为它们的功能太多了,我不想为我的网站增加太多复杂性。所以我觉得,对于我的需求,一个简单的静态文档生成器就可以满足。好吧,手动构建一个简单的生成器应该不会那么难。文本需求分析该生成器必须满足以下条件:从EJS模板生成HTML文件。有了布局文件,所有页面都应该有相同的页眉、页脚、导航等。允许可重用??的布局组件。有关站点的一般信息被封装到一个配置文件中。从JSON文件中读取数据。例如:一个项目列表,这样我就可以轻松地迭代和构建项目页面。为什么要使用EJS模板?因为EJS很简单,它只是嵌入HTML中的JavaScript。项目结构public/src/assets/data/pages/partials/layout.ejssite.config.jspublic:站点生成的地方。src:源文件。src/assets:包含CSS、JS、图片等。src/data:包含JSON数据。src/pages:里面根据EJS生成HTML页面的模板文件夹。src/layout.ejs:主要的原始页面模板,包含特殊的<%-body%>占位符,会插入特定的页面内容。site.config.js:模板中的全局配置文件。生成器生成器代码位于scripts/build.js文件中。每次要重建站点时,只需执行npmrunbuild命令即可。这是通过将以下脚本添加到package.json的脚本块来实现的:"build":"node./scripts/build"这是完整的生成器代码:constfse=require('fs-extra')constpath=require('path')const{promisify}=require('util')constejsRenderFile=promisify(require('ejs').renderFile)constglobP=promisify(require('glob'))constconfig=require('../site.config')constsrcPath='./src'constdistPath='./public'//清除目标文件夹fse.emptyDirSync(distPath)//复制资产文件夹fse.copy(`${srcPath}/assets`,`${distPath}/assets`)//读取页面模板globP('**/*.ejs',{cwd:`${srcPath}/pages`}).then((files)=>{files.forEach((file)=>{constfileData=path.parse(file)constdestPath=path.join(distPath,fileData.dir)//创建目标目录fse.mkdirs(destPath).then(()=>{//渲染页面返回ejsRenderFile(`${srcPath}/pages/${file}`,Object.assign({},config))}).then((pageContents)=>{//使用页面内容渲染布局returnejsRenderFile(`${srcPath}/layout.ejs`,Object.assign({},config,{body:pageContents}))}).then((layoutContent)=>{//保存html文件fse.writeFile(`${destPath}/${fileData.name}.html`,layoutContent)}).catch((err)=>{console.error(err)})})}).catch((err)=>{console.error(err)})接下来我会解释代码中的具体组件Dependencies我们只需要三个依赖:ejs将我们的模板编译成HTML。fs-extraNode文件模块的衍生版本,具有更多功能和增强的Promise支持。glob递归读取目录并返回所有匹配指定模式的文件,类型为数组。Promisify我们使用Node提供的util.promisify将所有回调函数转换为基于Promise的函数。它使我们的代码更短、更清晰且更易于阅读。const{promisify}=require('util')constejsRenderFile=promisify(require('ejs').renderFile)constglobP=promisify(require('glob'))加载配置然后将其注入模板渲染器。constconfig=require('../site.config')站点配置文件本身会加载其他JSON数据,例如:constprojects=require('./src/data/projects')module.exports={site:{title:'NanoGen',description:'MicroStaticSiteGeneratorinNode.js',projects}}清空站点文件夹我们使用fs-extra提供的emptyDirSync函数来清除生成的站点文件夹。fse.emptyDirSync(distPath)复制静态资源我们使用fs-extra提供的复制功能,递归地将静态资源复制到site文件夹。fse.copy(`${srcPath}/assets`,`${distPath}/assets`)编译页面模板首先,我们使用glob(promisified)递归读取src/pages文件夹以查找.ejs文件。它将返回与给定模式匹配的所有文件的数组。globP('**/*.ejs',{cwd:`${srcPath}/pages`}).then((files)=>{对于找到的每个模板文件,我们使用Node的path.parse函数来分离各种文件路径的组成部分(如目录、名称和扩展名)。然后我们使用fs-extra提供的mkdirs函数在站点目录中创建相应的文件夹。files.forEach((file)=>{constfileData=path.parse(file)constdestPath=path.join(distPath,fileData.dir)//创建目标目录fse.mkdirs(destPath)然后,我们使用EJS编译文件并将配置数据作为数据参数传递。由于我们是使用promified的ejs.renderFile函数,所以我们可以返回调用结果并在下一个promise链中处理结果。.then(()=>{//渲染页面returnejsRenderFile(`${srcPath}/pages/${file}`,Object.assign({},config))})在下一个then块中我们得到编译页面内容。现在,我们编译布局文件,将页面内容作为body属性传入。.then((pageContents)=>{//使用页面内容渲染布局returnejsRenderFile(`${srcPath}/layout.ejs`,Object.assign({},config,{body:pageContents}))})最后,我们得到生成的编译结果(布局+页面内容HTML),然后保存到对应的HTML文件中。.then((layoutContent)=>{//保存html文件fse.writeFile(`${destPath}/${fileData.name}.html`,layoutContent)})调试服务器添加一个简单的静态服务器到脚本package.json的。"serve":"serve./public"运行npmrunserve命令,打开http://localhost:5000,查看结果。进一步探索Markdown大多数静态文档生成器都支持以Markdown格式编写内容。并且,他们还支持在YAML格式的顶部添加一些元数据,就像这样:---title:HelloWorlddate:2013/7/1320:46:25---只需稍加修改,我们就可以支持Same现在运作。首先,我们必须添加两个依赖项:marked将markdown编译为HTML,front-matter从markdown中提取frontmatter。然后我们更新了glob的匹配模式以包含.md文件并保留.ejs以支持渲染复杂页面。如果要部署一些纯HTML页面,还需要包含.html。globP('**/*.@(md|ejs|html)',{cwd:`${srcPath}/pages`})对于每个文件,我们必须加载文件内容,以便我们可以在顶级数据。.then(()=>{//读取页面文件returnfse.readFile(`${srcPath}/pages/${file}`,'utf-8')})我们将加载的内容传递给前端。它将返回一个对象,其中属性属性是提取的元数据。然后我们使用这些数据来扩充站点配置。.then((data)=>{//提取frontmatterconstpageData=frontMatter(data)consttemplateConfig=Object.assign({},config,{page:pageData.attributes})现在我们根据fileextension编译成HTML,如果是.md,就用标记的函数编译;如果是.ejs,我们继续用EJS编译;如果是.html,就不用编译了。让pageContent切换(fileData.ext){case'.md':pageContent=marked(pageData.body)breakcase'.ejs':pageContent=ejs.render(pageData.body,templateConfig)breakdefault:pageContent=pageData.body}最后,我们像以前一样渲染布局。添加元数据最明显的意义就是我们可以为每个页面设置一个单独的标题,如下:---title:AnotherPage---让布局动态渲染这些数据:
