雖然現(xiàn)在都提倡前后端分離、node同構(gòu)直出,但是在很多公司的舊項(xiàng)目中,基于php的mvc框架項(xiàng)目仍然很多。在這種項(xiàng)目中,前端人員需要圍繞其中的view層進(jìn)行開發(fā),一些業(yè)務(wù)數(shù)據(jù)直接在模板中獲取。
下面舉一個(gè)例子,后臺(tái)采用laravel框架,前端模版引擎為blade模板。現(xiàn)在需要編寫一個(gè)模版,這個(gè)模板有以下邏輯:如果用戶已經(jīng)登錄,提示已經(jīng)登錄,否則提示未登錄。
<!--index.blade.php(laravel的模板文件以blade.php作為后綴)-->
...
@if(Auth::check())
<h1>已經(jīng)登錄</h1>
@else
<h1>未登錄</h1>
@endif
...
嗯,看上去除了增加blade引擎的模板語法外,其它跟寫法跟原生前端寫法并無差別。 由于嵌套了blade語法,導(dǎo)致這部分代碼只能被blade模板引擎解析,無法被其它項(xiàng)目共用。當(dāng)別的項(xiàng)目需要實(shí)現(xiàn)相同的頁面時(shí),只能另外寫一套。比如在discuz(開源的論壇框架)的頁面中,要實(shí)現(xiàn)上述例子,需要這樣寫:
<!--index.htm(discuz的模板文件以htm作為后綴)-->
...
<!--{if $_G['uid']}-->
<h1>已經(jīng)登錄</h1>
<!--{else}-->
<h1>未登錄</h1>
<!--{/if}-->
...
所以當(dāng)一個(gè)項(xiàng)目存在幾種模板引擎后,要實(shí)現(xiàn)模板的共用就成了一個(gè)大的問題,因?yàn)槊總€(gè)模板引擎有不同的模板語法。比如金蝶社區(qū),金蝶社區(qū)這個(gè)項(xiàng)目,一部分頁面用了laravel框架,一部分頁面用了discuz。在不同模板引擎中,有一些模版是重復(fù)出現(xiàn)的,比如頂部導(dǎo)航欄、footer等。由于業(yè)務(wù)需求不斷在變動(dòng),這些模塊一直在改變,如果仍然按照blade、discuz的語法寫兩套,維護(hù)起來勢(shì)必非常麻煩。
我們也做了一些嘗試,比如:
采用js生成html的方式
這種方式的前提是需要我們剔除后臺(tái)的模板語法,這樣才能被瀏覽器識(shí)別。然而在js中無法直接獲取后臺(tái)數(shù)據(jù),只能采用數(shù)據(jù)接口的方式異步請(qǐng)求、或者在html嵌入隱藏的input標(biāo)簽,通過id獲取input中的值來獲取后臺(tái)數(shù)據(jù)。然而這兩種方式對(duì)原有代碼的改動(dòng)非常大,還喪失了模板語法的優(yōu)勢(shì),需要額外寫dom操作,并會(huì)有種內(nèi)容加載很慢的感覺(js未引入,相關(guān)內(nèi)容就不會(huì)被渲染)。
優(yōu)雅的解決方案
為了實(shí)現(xiàn)跨項(xiàng)目的模版同步,我們?cè)谠屑軜?gòu)外增加了一個(gè)生成器。
我們?cè)O(shè)計(jì)了一個(gè)模板生成器,我們對(duì)它的期望是,在不改變?cè)械捻?xiàng)目結(jié)構(gòu)前提下,生成符合相應(yīng)模板語法的模板。它以一個(gè)源模板作為輸入,并輸出其他項(xiàng)目模板語法的模板。前端人員只需要編寫源模板,即可借助構(gòu)建工具生成編譯后的模板,并同步到項(xiàng)目中。
那具體怎么實(shí)現(xiàn)呢?下面還是以上面提到的例子作為講解。
首先在源模板文件上,應(yīng)該具有如下特點(diǎn):
可以根據(jù)不同的模板環(huán)境生成不同的模板#####
可能有點(diǎn)抽象,直接上代碼,應(yīng)該就會(huì)清晰明了了
<!--源模板 demo.html-->
...
**laravel?`@if(Auth::check())`:`<!--{if $_G['uid']}-->`**
<h1>已經(jīng)登錄</h1>
**laravel?`@else`:`<!--{else}-->`**
<h1>未登錄</h1>
**laravel?`@else`:`<!--{else}-->`**
...
可能你看到這里就有點(diǎn)疑惑,**號(hào)之間的內(nèi)容是什么鬼?如果去除星號(hào)后,你會(huì)發(fā)現(xiàn)這是一個(gè)三元運(yùn)算表達(dá)式!當(dāng)laravel變量為true時(shí)返回字符串@if(Auth::check()),否則返回,這樣的話,源模板就可以根據(jù)里面的變量值生成相應(yīng)的模板啦。
那你可能又會(huì)疑惑,怎么把這個(gè)變量傳進(jìn)去呢?
這里我們要寫一個(gè)renderTemplate方法:
@str:讀取文件生成的字符串
@data:傳遞進(jìn)去的變量
@callback:回調(diào)函數(shù),其中第一個(gè)參數(shù)就是編譯后的字符串
const renderTemplate = function(str,data,callback){
var a = str.replace(/\*\*(.*?)\*\*/g,function(res){//獲取包括**的內(nèi)容
var result = arguments[1]
//result匹配了**之間的內(nèi)容
try{
with(data){
var rel = eval(result)
//把字符串當(dāng)作表達(dá)式輸出,并用with修改作用域
return rel
}
}catch(e){
console.log(e)
//捕獲異常
}
})
callback&&callback(a)
}
原理就是用正則匹配出**號(hào)的內(nèi)容,并將里面的字符串當(dāng)作表達(dá)式執(zhí)行。 因而出了三元表達(dá)式,立即執(zhí)行的匿名函數(shù)也可以正常執(zhí)行。比如:
...
**(function(){ return 'david'})()**
...
會(huì)輸出
...
david
...
那renderTemplate具體怎么調(diào)用呢?
讓我們寫一個(gè)node腳本:
//index.js
const {readFileSync,writeFile} = require('fs')
var file ='/Users/david/demo.html' //文件絕對(duì)路徑
var content = readFileSync(file,'utf-8')//同步讀取文件
renderTemplate(content,{laravel:true},function(html){
console.log(html)//接下來就應(yīng)該做文件保存工作啦
})
renderTemplate(content,{laravel:false},function(html){
console.log(html)//接下來就應(yīng)該做文件保存工作啦
})
在執(zhí)行node index.js后,如無意外,你會(huì)在控制臺(tái)得到兩條輸出
現(xiàn)在的基本邏輯已經(jīng)跑通啦,剩下的就是文件存儲(chǔ)的操作啦。
進(jìn)階使用
以上只是一個(gè)簡(jiǎn)單的demo,每一個(gè)demo都要寫對(duì)應(yīng)的腳本邏輯才能用于實(shí)際項(xiàng)目中。在實(shí)際情況中,我們希望前端專注在源模板的編寫上,所以我們把node腳本進(jìn)行封裝。由于模板生成后的路徑、文件名不定、所以我們要把這一塊設(shè)置從腳本中分離出來,針對(duì)具體的源模板配置。
下面是我分離后的目錄結(jié)構(gòu):
dist:源模板輸出目錄
node_modules:模塊依賴目錄
template:源模板目錄,針對(duì)每一個(gè)源模板,配置有一個(gè)config.js和index.html
其中config.js是該源模板的相關(guān)配置參數(shù)。下面會(huì)詳細(xì)提到。
index.html是源模板文件
gulpfile.js是gulp的入口文件
package.json項(xiàng)目相關(guān)配置信息
首先查看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){//獲取包括**的內(nèi)容
var result = arguments[1]
//result匹配了**之間的內(nèi)容
try{
with(data){
var rel = eval(result)
//把字符串當(dāng)作表達(dá)式輸出,并用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//文件變動(dòng)路徑
const content = readFileSync(file,'utf-8')//同步讀取文件
const config = require(path.resolve(file,'../config.js'))//獲取同級(jí)目錄下的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)//有錯(cuò)誤輸出錯(cuò)誤
process.exit()
}
console.log('File ' + item.output.filename + '編譯成功');
})
})
})
});
})
里面的注釋應(yīng)該很清楚了,然后查看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'
}
}]
這個(gè)模塊實(shí)際上返回了一個(gè)數(shù)組,數(shù)組中的每一個(gè)對(duì)象對(duì)應(yīng)輸出的相關(guān)配置。data即renderTemplate的data參數(shù)。output則記錄了輸出的文件名和路徑。(注意:這里的path最好寫成絕對(duì)路徑)
在腳本執(zhí)行后,應(yīng)該會(huì)生成兩個(gè)模板,一個(gè)為"laravel_demo.blade.php",一個(gè)為"discuz_demo.blade.php".
項(xiàng)目github地址
https://github.com/David-zzg/kfc_module_creator
結(jié)語
至此,最核心的基本功能已經(jīng)完成了。然而,這并不意味著結(jié)束。在我們引入構(gòu)建工具的同時(shí),node已經(jīng)為我們打開一扇大門。我們可以在模板的基礎(chǔ)上再加些額外處理。比如壓縮文件、比如引入posthtml-bem實(shí)現(xiàn)css的命名管理等等!
最后說一句
這是我第一次寫技術(shù)類的博客,如有錯(cuò)誤之處,請(qǐng)大大們指出!只有寫過技術(shù)博客后,我才真正理解到寫技術(shù)博客的艱辛!致敬各位寫博客的大大們!