webpack-mvc 传统多页面组件化开发详解


Posted in Javascript onMay 07, 2019

最近有一个项目,还是使用的传统 MVC 模式开发,完全基于jQuery,使用了基于java模板引擎velocity,页面中嵌入了大量java语法,使得前后端分离不彻底,工程打包上线苦不堪言,为实现后端为服务化,前端也得彻底从后端中分离出来。

方案: webpack4 + ejs

webpack

  • 打包所有的 资源
  • 打包所以的 脚本
  • 打包所以的 图片
  • 打包所以的 样式
  • 打包所以的 表

ejs

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

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

监听文件变化

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

入口文件

入口文件长这样(可单一入口,也可多入口):

// 多入口
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] 对应入文件的 key 值,/ 分割文件夹。

最后就会在dist文件夹下生产文件:

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

既然是多页面开发,就要有多个入口,每个页面都要有自己对应的js入口,这样我们只需要遍历html文件,然后找到对应的js,处理成 entry 对象即可

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' 映射关系。

html(ejs) 组件化

页面框架

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、公共页面

header、footer每个页面都包含,所以放入主体框架页面内

3、页面各自部分

各个页面只需要写自己页面的html内容即可,并且还可以引入公共组件ejs

// 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 ?

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JQuery datepicker 使用方法
May 20 Javascript
a标签的href和onclick 的事件的区别介绍
Jul 26 Javascript
设置jsf的选择框h:selectOneMenu为不可编辑状态的方法
Jan 07 Javascript
jfreechart插件将数据展示成饼状图、柱状图和折线图
Apr 13 Javascript
javascript运算符——逻辑运算符全面解析
Jun 27 Javascript
js检测离开或刷新页面时表单数据是否更改的方法
Aug 02 Javascript
Vue.js计算属性computed与watch(5)
Dec 09 Javascript
js 实现获取name 相同的页面元素并循环遍历的方法
Feb 14 Javascript
JS实现发送短信验证后按钮倒计时功能(防止刷新倒计时失效)
Jul 07 Javascript
动态Axios的配置步骤详解
Jan 12 Javascript
Vue插件打包与发布的方法示例
Aug 20 Javascript
详解es6新增数组方法简便了哪些操作
May 09 Javascript
小程序扫描普通链接二维码跳转小程序指定界面方法
May 07 #Javascript
VUE接入腾讯验证码功能(滑块验证)备忘
May 07 #Javascript
vue接入腾讯防水墙代码
May 07 #Javascript
基于Vue实现的多条件筛选功能的详解(类似京东和淘宝功能)
May 07 #Javascript
详解vue中使用protobuf踩坑记
May 07 #Javascript
Node.js一行代码实现静态文件服务器的方法步骤
May 07 #Javascript
微信小程序扫描二维码获取信息实例详解
May 07 #Javascript
You might like
PHP 采集程序原理分析篇
2010/03/05 PHP
分享PHP header函数使用教程
2013/09/05 PHP
一个完整的php文件上传类实例讲解
2015/10/27 PHP
ThinkPHP5.0框架控制器继承基类和自定义类示例
2018/05/25 PHP
laravel5.6 框架邮件队列database驱动简单demo示例
2020/01/26 PHP
javascript getElementsByTagName
2011/01/31 Javascript
基于jQuery的自动完成插件
2011/02/03 Javascript
基于Jquery的文字自动截取(提供源代码)
2011/08/09 Javascript
THREE.JS入门教程(5)你应当知道的十件事
2013/01/24 Javascript
YUI模块开发原理详解
2013/11/18 Javascript
jQuery实现动画效果的简单实例
2014/01/27 Javascript
js代码实现的加入收藏效果并兼容主流浏览器
2014/06/23 Javascript
JavaScript基础语法、dom操作树及document对象
2014/12/02 Javascript
为JS扩展Array.prototype.indexOf引发的问题及解决办法
2015/01/21 Javascript
JQuery CheckBox(复选框)操作方法汇总
2015/04/15 Javascript
JS禁止查看网页源代码的实现方法
2016/10/12 Javascript
使用form-create动态生成vue自定义组件和嵌套表单组件
2019/01/18 Javascript
js设计模式之代理模式及订阅发布模式实例详解
2019/08/15 Javascript
Vue项目中使用better-scroll实现菜单映射功能方法
2019/09/11 Javascript
viewer.js一个强大的基于jQuery的图像查看插件(支持旋转、缩放)
2020/04/01 jQuery
JavaScript 实现下雪特效的示例代码
2020/09/09 Javascript
简单谈谈Python中函数的可变参数
2016/09/02 Python
Collatz 序列、逗号代码、字符图网格实例
2017/06/22 Python
使用python读取txt文件的内容,并删除重复的行数方法
2018/04/18 Python
Sanic框架Cookies操作示例
2018/07/17 Python
pycharm 将python文件打包为exe格式的方法
2019/01/16 Python
opencv3/Python 稠密光流calcOpticalFlowFarneback详解
2019/12/11 Python
python中shell执行知识点
2020/05/06 Python
css3的图形3d翻转效果应用示例
2014/04/08 HTML / CSS
澳大利亚优质的家居用品和生活方式公司:Bed Bath N’ Table
2019/04/16 全球购物
行政助理的岗位职责
2014/02/18 职场文书
群众路线教育实践活动剖析材料
2014/09/30 职场文书
三孔导游词
2015/02/05 职场文书
董事长秘书工作总结
2015/08/14 职场文书
《狼牙山五壮士》读后感:宁死不屈,视死如归
2019/08/16 职场文书
Redis Lua脚本实现ip限流示例
2022/07/15 Redis