mvc框架中一种优雅的模板同步方案

虽然现在都提倡前后端分离、node同构直出,但是在很多公司的旧项目中,基于php的mvc框架项目仍然很多。在这种项目中,前端人员需要围绕其中的view层进行开发,一些业务数据直接在模板中获取。
下面举一个例子,后台采用laravel框架,前端模版引擎为blade模板。现在需要编写一个模版,这个模板有以下逻辑:如果用户已经登录,提示已经登录,否则提示未登录。

 <!--index.blade.php(laravel的模板文件以blade.php作为后缀)-->
...
    @if(Auth::check())
         <h1>已经登录</h1>
    @else
          <h1>未登录</h1>
    @endif
...

嗯,看上去除了增加blade引擎的模板语法外,其它跟写法跟原生前端写法并无差别。 由于嵌套了blade语法,导致这部分代码只能被blade模板引擎解析,无法被其它项目共用。当别的项目需要实现相同的页面时,只能另外写一套。比如在discuz(开源的论坛框架)的页面中,要实现上述例子,需要这样写:

<!--index.htm(discuz的模板文件以htm作为后缀)-->
...
     <!--{if $_G['uid']}-->
         <h1>已经登录</h1>
    <!--{else}-->
          <h1>未登录</h1>
    <!--{/if}-->
...

所以当一个项目存在几种模板引擎后,要实现模板的共用就成了一个大的问题,因为每个模板引擎有不同的模板语法。比如金蝶社区,金蝶社区这个项目,一部分页面用了laravel框架,一部分页面用了discuz。在不同模板引擎中,有一些模版是重复出现的,比如顶部导航栏、footer等。由于业务需求不断在变动,这些模块一直在改变,如果仍然按照blade、discuz的语法写两套,维护起来势必非常麻烦。

我们也做了一些尝试,比如:

采用js生成html的方式

这种方式的前提是需要我们剔除后台的模板语法,这样才能被浏览器识别。然而在js中无法直接获取后台数据,只能采用数据接口的方式异步请求、或者在html嵌入隐藏的input标签,通过id获取input中的值来获取后台数据。然而这两种方式对原有代码的改动非常大,还丧失了模板语法的优势,需要额外写dom操作,并会有种内容加载很慢的感觉(js未引入,相关内容就不会被渲染)。

优雅的解决方案

为了实现跨项目的模版同步,我们在原有架构外增加了一个生成器。

我们设计了一个模板生成器,我们对它的期望是,在不改变原有的项目结构前提下,生成符合相应模板语法的模板。它以一个源模板作为输入,并输出其他项目模板语法的模板。前端人员只需要编写源模板,即可借助构建工具生成编译后的模板,并同步到项目中。

那具体怎么实现呢?下面还是以上面提到的例子作为讲解。
首先在源模板文件上,应该具有如下特点:

可以根据不同的模板环境生成不同的模板#####

可能有点抽象,直接上代码,应该就会清晰明了了

 <!--源模板 demo.html-->
...
    **laravel?`@if(Auth::check())`:`<!--{if $_G['uid']}-->`**
    <h1>已经登录</h1>
    **laravel?`@else`:`<!--{else}-->`**
    <h1>未登录</h1>
    **laravel?`@else`:`<!--{else}-->`**

...


可能你看到这里就有点疑惑,**号之间的内容是什么鬼?如果去除星号后,你会发现这是一个三元运算表达式!当laravel变量为true时返回字符串@if(Auth::check()),否则返回,这样的话,源模板就可以根据里面的变量值生成相应的模板啦。
那你可能又会疑惑,怎么把这个变量传进去呢?
这里我们要写一个renderTemplate方法:

@str:读取文件生成的字符串
@data:传递进去的变量
@callback:回调函数,其中第一个参数就是编译后的字符串
const renderTemplate = function(str,data,callback){
    var a = str.replace(/\*\*(.*?)\*\*/g,function(res){//获取包括**的内容
        var result = arguments[1]
        //result匹配了**之间的内容
        try{
            with(data){
                var rel = eval(result)
                //把字符串当作表达式输出,并用with修改作用域
                return rel
            }
        }catch(e){
            console.log(e)
            //捕获异常
        }
    })
    callback&&callback(a)
}

原理就是用正则匹配出**号的内容,并将里面的字符串当作表达式执行。 因而出了三元表达式,立即执行的匿名函数也可以正常执行。比如:

...
**(function(){ return 'david'})()**
...

会输出

...
david
...

那renderTemplate具体怎么调用呢?
让我们写一个node脚本:

//index.js
const {readFileSync,writeFile} = require('fs')
var file ='/Users/david/demo.html' //文件绝对路径
var content = readFileSync(file,'utf-8')//同步读取文件
renderTemplate(content,{laravel:true},function(html){
    console.log(html)//接下来就应该做文件保存工作啦
})
renderTemplate(content,{laravel:false},function(html){
    console.log(html)//接下来就应该做文件保存工作啦
})

在执行node index.js后,如无意外,你会在控制台得到两条输出

控制台截图

现在的基本逻辑已经跑通啦,剩下的就是文件存储的操作啦。

进阶使用

以上只是一个简单的demo,每一个demo都要写对应的脚本逻辑才能用于实际项目中。在实际情况中,我们希望前端专注在源模板的编写上,所以我们把node脚本进行封装。由于模板生成后的路径、文件名不定、所以我们要把这一块设置从脚本中分离出来,针对具体的源模板配置。

下面是我分离后的目录结构:

目录结构

dist:源模板输出目录
node_modules:模块依赖目录
template:源模板目录,针对每一个源模板,配置有一个config.js和index.html
其中config.js是该源模板的相关配置参数。下面会详细提到。
index.html是源模板文件
gulpfile.js是gulp的入口文件
package.json项目相关配置信息

首先查看gulpfile.js

const gulp = require('gulp')
const watch = require('gulp-watch')
const {readFileSync,writeFile} = require('fs')
const path = require('path')
gulp.task('default',['build'])
const renderTemplate = function(str,data,callback){
    var a = str.replace(/\*\*(.*?)\*\*/g,function(res){//获取包括**的内容
        var result = arguments[1]
        //result匹配了**之间的内容
        try{
            with(data){
                var rel = eval(result)
                //把字符串当作表达式输出,并用with修改作用域
                return rel
            }
        }catch(e){
            console.log(e)
            //捕获异常
        }
    })
    callback&&callback(a)
}

gulp.task('build',()=>{
    watch('template/**/*.html',{ ignoreInitial: false },function(event) {
        let file = event.path//文件变动路径
        const content = readFileSync(file,'utf-8')//同步读取文件
        const config = require(path.resolve(file,'../config.js'))//获取同级目录下的config文件
        config.forEach(item=>{
            renderTemplate(content,item.data,function(str){
                writeFile(path.resolve(item.output.path,item.output.filename),str,function(err){
                    if(err){
                        console.log(err)//有错误输出错误
                        process.exit()
                    }
                    console.log('File ' + item.output.filename + '编译成功');
                })
            })
        })
    });
})

里面的注释应该很清楚了,然后查看demo中的config.js

module.exports = [{
    data:{
        laravel:true
    },
    output:{
        filename:'laravel_demo.blade.php',
        path:'dist'
    }
},{
    data:{
        laravel:false
    },
    output:{
        filename:'discuz_demo.blade.php',
        path:'dist'
    }
}]

这个模块实际上返回了一个数组,数组中的每一个对象对应输出的相关配置。data即renderTemplate的data参数。output则记录了输出的文件名和路径。(注意:这里的path最好写成绝对路径)

在脚本执行后,应该会生成两个模板,一个为"laravel_demo.blade.php",一个为"discuz_demo.blade.php".

项目github地址

https://github.com/David-zzg/kfc_module_creator

结语

至此,最核心的基本功能已经完成了。然而,这并不意味着结束。在我们引入构建工具的同时,node已经为我们打开一扇大门。我们可以在模板的基础上再加些额外处理。比如压缩文件、比如引入posthtml-bem实现css的命名管理等等!

最后说一句

这是我第一次写技术类的博客,如有错误之处,请大大们指出!只有写过技术博客后,我才真正理解到写技术博客的艰辛!致敬各位写博客的大大们!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容