webpack-mvc 傳統(tǒng)多頁(yè)面組件化開發(fā)詳解
最近有一個(gè)項(xiàng)目,還是使用的傳統(tǒng) MVC 模式開發(fā),完全基于jQuery,使用了基于java模板引擎velocity,頁(yè)面中嵌入了大量java語(yǔ)法,使得前后端分離不徹底,工程打包上線苦不堪言,為實(shí)現(xiàn)后端為服務(wù)化,前端也得徹底從后端中分離出來(lái)。
方案: webpack4 + ejs
webpack
- 打包所有的 資源
- 打包所以的 腳本
- 打包所以的 圖片
- 打包所以的 樣式
- 打包所以的 表
ejs
高效的 JavaScript 模板引擎,代替 velocity
webpack 配置
基本插件
- @babel/core,@babel/preset-env,babel-loader
es6 語(yǔ)法轉(zhuǎn)譯
- css-loader,style-loader
編譯打包c(diǎn)ss
- node-sass,sass-loader
解析sass
- postcss-loader,autoprefixer
自動(dòng)給樣式增加瀏覽器前綴
- mini-css-extract-plugin
將css從js中抽離出來(lái)為單獨(dú)文件
- optimize-css-assets-webpack-plugin
壓縮css
- uglifyjs-webpack-plugin
壓縮js
- ejs-loader
解析ejs模板文件
- html-webpack-plugin
生成html文件
- rimraf
刪除文件、文件夾
- watch
監(jiān)聽文件變化
上面是一些要用的插件,具體用法不累述。
入口文件
入口文件長(zhǎng)這樣(可單一入口,也可多入口):
// 多入口 entry: { pageA: './src/pageA/index.js', pageB: './src/pageB/index.js', 'pageC/login': './src/pageC/login/login.js' }
出口文件:
output: { filename: '[name].js', path: path.resolve(__dirname, '../dist'), }
filename 值中的 [name] 對(duì)應(yīng)入文件的 key 值,/ 分割文件夾。
最后就會(huì)在dist文件夾下生產(chǎn)文件:
- dist/pageA/index.js
- dist/pageB/index.js
- dist/pageC/login/login.js
既然是多頁(yè)面開發(fā),就要有多個(gè)入口,每個(gè)頁(yè)面都要有自己對(duì)應(yīng)的js入口,這樣我們只需要遍歷html文件,然后找到對(duì)應(yīng)的js,處理成 entry 對(duì)象即可
const path = require('path') const glob = require('glob') const pages = (entries => { let entry = {}, htmlArr = [] // 格式化生成入口 entries.forEach((file) => { // ...../webpack-mvc/src/page/pageA/index.html const fileSplit = file.split('/') const length = fileSplit.length // 頁(yè)面入口 pageA/index.html const filePath = fileSplit.slice(length - 2, length).join('/') // 根據(jù)html路徑找到對(duì)應(yīng)的js路徑,js可以和html放在同一文件夾,也可單獨(dú)放在一個(gè)文件夾內(nèi),只要能找到 const jsPath = path.resolve(__dirname, `../src/page/${filePath.split('.')[0]}.js`) // _main.ejs 頁(yè)面主題框架,html組件化 pageHtml = path.resolve(__dirname, '../src/_main.ejs') if (!fs.existsSync(jsPath)) { return; } entry['js/' + filePath.split('.')[0]] = jsPath // 加 js/ 即表示將打包后的js單獨(dú)放在一個(gè)文件夾內(nèi) }) return entry })(glob(path.resolve(__dirname, '../src/page/*/*.html'), {sync: true}))
上面只是本例的目錄結(jié)構(gòu),根據(jù)不同的目錄結(jié)構(gòu),更改路徑即可,目的就是得到 ‘js打包生成路徑': ‘入口js' 映射關(guān)系。
html(ejs) 組件化
頁(yè)面框架
1、主體框架 src/_main.ejs
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <div class="main-head"> <%= require('@/common/components/header/header.ejs')() %> </div> <div class="main-content"> <%= htmlWebpackPlugin.options.content %> </div> <div class="main-foot"> <%= require('@/common/components/footer/footer.ejs')() %> </div> </body> </html>
2、公共頁(yè)面
header、footer每個(gè)頁(yè)面都包含,所以放入主體框架頁(yè)面內(nèi)
3、頁(yè)面各自部分
各個(gè)頁(yè)面只需要寫自己頁(yè)面的html內(nèi)容即可,并且還可以引入公共組件ejs
// pageA/index.html <div> <h1>pageA index</h1> </div> // pageA/login.html <div> <%= require('@/common/components/form.ejs')() %> <h1>pageA login</h1> </div>
網(wǎng)上查了很多資料,沒找到可以實(shí)現(xiàn)上面步驟的方法,基本都是要在每個(gè)頁(yè)面的js里去寫一些ejs語(yǔ)法,做不到我想要的只關(guān)注此頁(yè)面本身的內(nèi)容。
替換 _main.ejs,生成臨時(shí)模板
我的解決方法是 通過 node 讀取頁(yè)面 html 文件,然后替換 _main.ejs 中的 content 部分,生成一個(gè)臨時(shí) ejs 模板文件,然后通過插件 html-webpack-plugin 生成最終頁(yè)面 html 文件
function createTemplate(file, jsPath, entry) { let obj = { title: '', template: '', filename: '', chunks: [jsPath] } // _main.ejs 頁(yè)面主題框架,html組件化 let mainHtml = path.resolve(__dirname, '../src/_main.ejs') let fileSplit = file.split('/') // html 生成路徑 let filename = fileSplit.slice(fileSplit.length - 2).join('/').split('.')[0]; let strContent = fs.readFileSync(file, 'utf-8') let strMain = fs.readFileSync(mainHtml, 'utf-8') let template = fileSplit.slice(fileSplit.length - 2).join('_').split('.')[0]; strMain = strMain.replace(/<%= htmlWebpackPlugin.options.content %>/, strContent) fs.writeFileSync(path.resolve(__dirname, `../src/template/template_${template}.ejs`), strMain) obj.template = path.resolve(__dirname, `../src/template/template_${template}.ejs`) obj.filename = filename return obj }
有了上面方法的思路,我們可以在各自頁(yè)面中做更多的操作
頁(yè)面 title
// pageA/index.html <%=title 頁(yè)面A %> <div> <h1>pageA index</h1> </div>
頁(yè)面直接引入js,只壓縮不打包
// pageA/index.html <%=title 頁(yè)面A %> <div> <h1>pageA index</h1> </div> <script src="js/common/util.js"></script> <script src="js/common/server.api.js"></script>
這里引入js的路徑是最終文件壓縮生成的位置(dist目錄下),因?yàn)殚_發(fā)模式和生產(chǎn)環(huán)境路徑有所不同,所以等下在代碼中要區(qū)別不同環(huán)境去替換不同的路徑。
頁(yè)面引入ejs組件
// pageA/index.html <%=title 頁(yè)面A %> <div> <%= require('@/common/components/form.ejs')() %> <h1>pageA index</h1> </div> <script src="js/common/util.js"></script> <script src="js/common/server.api.js"></script>
page.config.js
const fs = require('fs') const path = require('path') const glob = require('glob') if (process.env.NODE_ENV === 'development') { const rimraf = require('rimraf') rimraf.sync(path.resolve(__dirname, '../src/template/*'), fs, function cb() { console.log('template目錄已清空') }) } const pages = (entries => { let entry = {}, htmlArr = [] // 格式化生成入口 entries.forEach((file) => { // ...../webpack-mvc/src/page/pageA/index.html let fileSplit = file.split('/') let length = fileSplit.length // 頁(yè)面入口 page/pageA/index.html let filePath = fileSplit.slice(length - 3, length).join('/') // 根據(jù)html路徑找到對(duì)應(yīng)的js路徑,js可以和html放在同一文件夾,也可單獨(dú)放在一個(gè)文件夾內(nèi),只要能找到 let jsFile = path.resolve(__dirname, `../src/${filePath.split('.')[0]}.js`) if (!fs.existsSync(jsFile)) { return; } let jsPath = 'js/' + filePath.split('.')[0] entry['js/' + filePath.split('.')[0]] = jsFile htmlArr.push(createTemplate(file, jsPath, entry)) }) return {entry, htmlArr} })(glob(path.resolve(__dirname, '../src/page/*/*.html'), {sync: true})) function scriptLinkEntry(entry, file) { // file: /js/common/js/util.js let fileNew = './src/' + file.split('/').slice(2).join('/') let fileSplit = fileNew.split('/') entry['js/common/' + fileSplit.slice(fileSplit.length - 1).join('/').replace('.js', '')] = fileNew } function replaceScript(content, entry) { let scriptLink = content.match(/<script.*src=["|'](.*)["|']><\/script>/g) if (scriptLink) { scriptLink.forEach(item => { // src: /js/common/js/util.js let src = item.match(/src=["|'](.*)["|']/)[1]; scriptLinkEntry(entry, src) let scriptlinNew = src // 生產(chǎn)環(huán)境根據(jù)頁(yè)面路徑找到j(luò)s的相對(duì)路徑,開發(fā)環(huán)境 /js/ 指向 dist 目錄下 js 文件夾 if (process.env.NODE_ENV === 'production') { let srcSplit = src.split('/') srcSplit.splice(3, 1) // ['', 'js', 'common', 'util.js'] scriptLinkNew = `..${srcSplit.join('/')}` // ../js/common/util.js } content = content.replace(src, scriptLinkNew) }) } return content; } function createTemplate(file, jsPath, entry) { let obj = { title: '', template: '', filename: '', chunks: [jsPath] } // _main.ejs 頁(yè)面主題框架,html組件化 let mainHtml = path.resolve(__dirname, '../src/_main.ejs') let fileSplit = file.split('/') // html 生成路徑 let filename = fileSplit.slice(fileSplit.length - 2).join('/').split('.')[0]; let strContent = fs.readFileSync(file, 'utf-8') let strMain = fs.readFileSync(mainHtml, 'utf-8') let template = fileSplit.slice(fileSplit.length - 2).join('_').split('.')[0] // 提取頁(yè)面title let titleMatch = strContent.match(/<%=title(.*)%>/) let title = '' if (titleMatch) { title = titleMatch[1] strContent = strContent.replace(/<%=title(.*)%>/, '') } // 提取頁(yè)面與主體框架中引入的靜態(tài)js文件,將其放入入口文件中經(jīng)行壓縮,并適應(yīng)開發(fā)與生產(chǎn)路徑 strMain = replaceScript(strMain, entry) strContent = replaceScript(strContent, entry) strMain = strMain.replace(/<%= htmlWebpackPlugin.options.content %>/, strContent) fs.writeFileSync(path.resolve(__dirname, `../src/template/template_${template}.ejs`), strMain) obj.title = title obj.template = path.resolve(__dirname, `../src/template/template_${template}.ejs`) obj.filename = filename return obj } module.exports = pages;
熱刷新
此時(shí)熱刷新只能監(jiān)聽到j(luò)s和css的改變,因?yàn)槟0迨莿?dòng)態(tài)生成的,更改頁(yè)面內(nèi)容時(shí)模板并沒有改變,所以無(wú)法觸發(fā)devServer的熱刷新,手動(dòng)刷新也不會(huì)有變化,因?yàn)榕R時(shí)模板文件沒有改變,借用插件 watch 來(lái)監(jiān)聽html文件變化,然后重寫模板文件可解決問題。
const fs = require('fs') const path = require('path') const watch = require('watch') const { replaceScript } = require('./page.config.js') watch.watchTree(path.resolve(__dirname, '../src/page'), (f, curr, prev) => { if (typeof f == 'object' && prev === null && curr === null) { // Finished walking the tree } else if (prev === null) { // f is a new file createTemplate(f) } else if (curr.link === 0) { // f was removed } else { createTemplate(f) } }) function createTemplate(file) { if (file.indexOf('.html') === -1) { return } console.log('file', file) let mainHtml = path.resolve(__dirname, '../src/_main.ejs') let strContent = fs.readFileSync(file, 'utf-8') let strMain = fs.readFileSync(mainHtml, 'utf-8') let template = file.split('\\').slice(file.split('\\').length - 2).join('_').split('.')[0] // 提取頁(yè)面與主體框架中引入的靜態(tài)js文件,將其放入入口文件中經(jīng)行壓縮,并適應(yīng)開發(fā)與生產(chǎn)路徑 // 這里不再處理 title 和 靜態(tài)js 入口壓縮 strMain = replaceScript(strMain, {}, true) strContent = replaceScript(strContent, {}, true) strContent = strContent.replace(/<%=(.*)%>/, '') strMain = strMain.replace(/<%= htmlWebpackPlugin.options.content %>/, strContent) fs.writeFileSync(path.resolve(__dirname, `../src/template/template_${template}.ejs`), strMain) }
這里不再處理title和靜態(tài)js入口壓縮,更改了這些只能再重新 npm run dev
國(guó)際化
const languageProperty = require('../properties/language.properties.js') function getLanText(val) { let lan = 'zh' // $.cookie('lan') let str = languageProperty[val] && languageProperty[val][lan] || val let defaultOpt = languageProperty[val] && languageProperty[val]['default'] let opts = defaultOpt && $.extend(true, [], defaultOpt) opts ? opts.unshift('') : false let args = opts && arguments.length === 1 ? opts : arguments if (args.length > 1) { let params = Array.property.slice.call(args, 1) return str.replace(/{(\d+)}/g, function(curr, index) { return params[index] }) } else { return str } } function translateAll() { let num = $('html').find('[lang]').length let count = 0 if (num === 0) { $('body').show() } $('html').find('[lang]').each(function() { count += 1; let lang = $(this).attr('lang') if (lang === '') { return; } let nodeName = $(this)[0].nodeName let text = getLanText(lang) // 簡(jiǎn)單處理,復(fù)雜的可再這里更改 if (nodeName === 'INPUT') { $(this).attr('placeholder', text) } else { $(this).html(text) } if (count === num) { $('body').show() } }) } module.exports = { getLanText, translateAll }
在header.js里調(diào)用一次就可以了。
結(jié)語(yǔ)
至此,傳統(tǒng)多頁(yè)面組件化開發(fā)流程基本完成,可以完全脫離后臺(tái)愉快的開發(fā)前端了,拋棄eclipse,擁抱vsCode。
此文只構(gòu)建了基本的框架,中間還有很多優(yōu)化點(diǎn),打包速度,公共代碼等等都沒有去細(xì)究,等頁(yè)面、代碼量增加,這也是必須去研究的,路漫漫其修遠(yuǎn)兮。
Guthub 可直接 npm run dev, npm run build 運(yùn)行, 順便求個(gè)Star 😄
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
JS判斷非空至少輸入兩個(gè)字符的簡(jiǎn)單實(shí)現(xiàn)方法
這篇文章主要介紹了JS判斷非空至少輸入兩個(gè)字符的簡(jiǎn)單實(shí)現(xiàn)方法,需要的朋友可以參考下2017-06-06微信小程序全局變量改變監(jiān)聽的實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于微信小程序全局變量改變監(jiān)聽的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用微信小程序具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07JavaScript常用標(biāo)簽和方法總結(jié)
JavaScript可以被瀏覽器直接解釋執(zhí)行,它可以更好得減小服務(wù)器壓力,提高程序運(yùn)行效率,下面小編通過本篇文章給大家分享JavaScript常用標(biāo)簽和方法,需要的朋友一起來(lái)學(xué)習(xí)吧2015-09-09JavaScript+CSS無(wú)限極分類效果完整實(shí)現(xiàn)方法
這篇文章主要介紹了JavaScript+CSS無(wú)限極分類效果完整實(shí)現(xiàn)方法,涉及JavaScript針對(duì)頁(yè)面元素節(jié)點(diǎn)遍歷與動(dòng)態(tài)操作技巧,需要的朋友可以參考下2015-12-12JavaScript參數(shù)個(gè)數(shù)可變的函數(shù)舉例說(shuō)明
JavaScript允許一個(gè)函數(shù)傳遞個(gè)數(shù)可變的參數(shù),因?yàn)橛衋rguments這個(gè)內(nèi)置對(duì)象,它一個(gè)函數(shù)傳遞的所有參數(shù)的數(shù)組2014-10-10javascript實(shí)現(xiàn)跳轉(zhuǎn)菜單的具體方法
這篇文章介紹了javascript實(shí)現(xiàn)跳轉(zhuǎn)菜單的具體方法,有需要的朋友可以參考一下2013-07-07js監(jiān)聽元素是否出現(xiàn)在可視區(qū)域詳解(IntersectionObserver)
這篇文章主要給大家介紹了關(guān)于js監(jiān)聽元素是否出現(xiàn)在可視區(qū)域(IntersectionObserver)的相關(guān)資料, IntersectionObserver是一個(gè)JavaScript API,用于監(jiān)測(cè)一個(gè)元素與其父元素或視窗的交叉狀態(tài),需要的朋友可以參考下2024-06-06