webpack-mvc 傳統多頁面組件化開發

最近有一個項目,仍是使用的傳統 MVC 模式開發,徹底基於jQuery,使用了基於java模板引擎velocity,頁面中嵌入了大量java語法,使得先後端分離不完全,工程打包上線苦不堪言,爲實現後端爲服務化,前端也得完全從後端中分離出來。css

方案: webpack4 + ejs

webpack

  • 打包全部的 資源
  • 打包因此的 腳本
  • 打包因此的 圖片
  • 打包因此的 樣式
  • 打包因此的 表

ejs

高效的 JavaScript 模板引擎,代替 velocityhtml

webpack 配置

基本插件

  • @babel/core,@babel/preset-env,babel-loader
    es6 語法轉譯
  • css-loader,style-loader
    編譯打包css
  • node-sass,sass-loader
    解析sass
  • postcss-loader,autoprefixer
    自動給樣式增長瀏覽器前綴
  • mini-css-extract-plugin
    將css從js中抽離出來爲單獨文件
  • optimize-css-assets-webpack-plugin
    壓縮css
  • uglifyjs-webpack-plugin
    壓縮js
  • ejs-loader
    解析ejs模板文件
  • html-webpack-plugin
    生成html文件
  • rimraf
    刪除文件、文件夾
  • watch
    監聽文件變化

上面是一些要用的插件,具體用法不累述。前端

入口文件

入口文件長這樣(可單一入口,也可多入口):java

// 多入口
entry: {
  pageA: './src/pageA/index.js',
  pageB: './src/pageB/index.js',
  'pageC/login': './src/pageC/login/login.js'
}
複製代碼

出口文件:node

output: {
  filename: '[name].js',
  path: path.resolve(__dirname, '../dist'),
}
複製代碼

filename 值中的 [name] 對應入文件的 key 值,/ 分割文件夾。
最後就會在dist文件夾下生產文件:webpack

  • dist/pageA/index.js
  • dist/pageB/index.js
  • dist/pageC/login/login.js

既然是多頁面開發,就要有多個入口,每一個頁面都要有本身對應的js入口,這樣咱們只須要遍歷html文件,而後找到對應的js,處理成 entry 對象便可git

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

    // 頁面入口 pageA/index.html
    const filePath = fileSplit.slice(length - 2, length).join('/') 

    // 根據html路徑找到對應的js路徑,js能夠和html放在同一文件夾,也可單獨放在一個文件夾內,只要能找到 
    const jsPath = path.resolve(__dirname, `../src/page/${filePath.split('.')[0]}.js`) 

    // _main.ejs 頁面主題框架,html組件化
    pageHtml = path.resolve(__dirname, '../src/_main.ejs') 

    if (!fs.existsSync(jsPath)) {
      return;
    }
    entry['js/' + filePath.split('.')[0]] = jsPath // 加 js/ 即表示將打包後的js單獨放在一個文件夾內
  })
  return entry
})(glob(path.resolve(__dirname, '../src/page/*/*.html'), {sync: true}))
複製代碼

上面只是本例的目錄結構,根據不一樣的目錄結構,更改路徑便可,目的就是獲得 ‘js打包生成路徑’: ‘入口js’ 映射關係。es6

html(ejs) 組件化

頁面框架

一、主體框架 src/_main.ejsgithub

<!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>
複製代碼

二、公共頁面
header、footer每一個頁面都包含,因此放入主體框架頁面內
三、頁面各自部分
各個頁面只須要寫本身頁面的html內容便可,而且還能夠引入公共組件ejsweb

// pageA/index.html
<div>
  <h1>pageA index</h1>
</div>

// pageA/login.html
<div>
  <%= require('@/common/components/form.ejs')() %>
  <h1>pageA login</h1>
</div>
複製代碼

網上查了不少資料,沒找到能夠實現上面步驟的方法,基本都是要在每一個頁面的js裏去寫一些ejs語法,作不到我想要的只關注此頁面自己的內容。

替換 _main.ejs,生成臨時模板

個人解決方法是 經過 node 讀取頁面 html 文件,而後替換 _main.ejs 中的 content 部分,生成一個臨時 ejs 模板文件,而後經過插件 html-webpack-plugin 生成最終頁面 html 文件

function createTemplate(file, jsPath, entry) {
  let obj = {
    title: '',
    template: '',
    filename: '',
    chunks: [jsPath]
  }
  // _main.ejs 頁面主題框架,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
}
複製代碼

有了上面方法的思路,咱們能夠在各自頁面中作更多的操做

頁面 title

// pageA/index.html

<%=title 頁面A %>
<div>
  <h1>pageA index</h1>
</div>
複製代碼

頁面直接引入js,只壓縮不打包

// pageA/index.html

<%=title 頁面A %>

<div>
  <h1>pageA index</h1>
</div>

<script src="js/common/util.js"></script>
<script src="js/common/server.api.js"></script>
複製代碼

這裏引入js的路徑是最終文件壓縮生成的位置(dist目錄下),由於開發模式和生產環境路徑有所不一樣,因此等下在代碼中要區別不一樣環境去替換不一樣的路徑。

頁面引入ejs組件

// pageA/index.html

<%=title 頁面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

    // 頁面入口 page/pageA/index.html
    let filePath = fileSplit.slice(length - 3, length).join('/')

    // 根據html路徑找到對應的js路徑,js能夠和html放在同一文件夾,也可單獨放在一個文件夾內,只要能找到
    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
      // 生產環境根據頁面路徑找到js的相對路徑,開發環境 /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 頁面主題框架,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]

  // 提取頁面title
  let titleMatch = strContent.match(/<%=title(.*)%>/)
  let title = ''
  if (titleMatch) {
    title = titleMatch[1]
    strContent = strContent.replace(/<%=title(.*)%>/, '')
  }

  // 提取頁面與主體框架中引入的靜態js文件,將其放入入口文件中經行壓縮,並適應開發與生產路徑
  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;
複製代碼

熱刷新

此時熱刷新只能監聽到js和css的改變,由於模板是動態生成的,更改頁面內容時模板並無改變,因此沒法觸發devServer的熱刷新,手動刷新也不會有變化,由於臨時模板文件沒有改變,借用插件 watch 來監聽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]
  // 提取頁面與主體框架中引入的靜態js文件,將其放入入口文件中經行壓縮,並適應開發與生產路徑
  // 這裏再也不處理 title 和 靜態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和靜態js入口壓縮,更改了這些只能再從新 npm run dev

國際化

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)
    // 簡單處理,複雜的可再這裏更改
    if (nodeName === 'INPUT') {
      $(this).attr('placeholder', text)
    } else {
      $(this).html(text)
    }
    if (count === num) {
      $('body').show()
    }
  })
}

module.exports = { getLanText, translateAll }
複製代碼

在header.js裏調用一次就能夠了。

結語

至此,傳統多頁面組件化開發流程基本完成,能夠徹底脫離後臺愉快的開發前端了,拋棄eclipse,擁抱vsCode。
此文只構建了基本的框架,中間還有不少優化點,打包速度,公共代碼等等都沒有去細究,等頁面、代碼量增長,這也是必須去研究的,路漫漫其修遠兮。
Guthub 可直接 npm run dev, npm run build 運行, 順便求個Star 😄

相關文章
相關標籤/搜索