经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » webpack » 查看文章
webpack-mvc 传统多页面组件化开发详解
来源:jb51  时间:2019/5/7 12:13:37  对本文有异议

最近有一个项目,还是使用的传统 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

监听文件变化

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

入口文件

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

  1. // 多入口
  2. entry: {
  3. pageA: './src/pageA/index.js',
  4. pageB: './src/pageB/index.js',
  5. 'pageC/login': './src/pageC/login/login.js'
  6. }

出口文件:

  1. output: {
  2. filename: '[name].js',
  3. path: path.resolve(__dirname, '../dist'),
  4. }

filename 值中的 [name] 对应入文件的 key 值,/ 分割文件夹。

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

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

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

  1. const path = require('path')
  2. const glob = require('glob')
  3.  
  4. const pages = (entries => {
  5. let entry = {}, htmlArr = []
  6. // 格式化生成入口
  7. entries.forEach((file) => {
  8. // ...../webpack-mvc/src/page/pageA/index.html
  9. const fileSplit = file.split('/')
  10. const length = fileSplit.length
  11.  
  12. // 页面入口 pageA/index.html
  13. const filePath = fileSplit.slice(length - 2, length).join('/')
  14.  
  15. // 根据html路径找到对应的js路径,js可以和html放在同一文件夹,也可单独放在一个文件夹内,只要能找到
  16. const jsPath = path.resolve(__dirname, `../src/page/${filePath.split('.')[0]}.js`)
  17.  
  18. // _main.ejs 页面主题框架,html组件化
  19. pageHtml = path.resolve(__dirname, '../src/_main.ejs')
  20.  
  21. if (!fs.existsSync(jsPath)) {
  22. return;
  23. }
  24. entry['js/' + filePath.split('.')[0]] = jsPath // 加 js/ 即表示将打包后的js单独放在一个文件夹内
  25. })
  26. return entry
  27. })(glob(path.resolve(__dirname, '../src/page/*/*.html'), {sync: true}))

上面只是本例的目录结构,根据不同的目录结构,更改路径即可,目的就是得到 ‘js打包生成路径': ‘入口js' 映射关系。

html(ejs) 组件化

页面框架

1、主体框架 src/_main.ejs

  1. <!DOCTYPE html>
  2. <html lang="en">
  3.  
  4. <head>
  5. <meta charset="utf-8">
  6. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  7. <meta name="viewport" content="width=device-width,initial-scale=1.0">
  8. <title><%= htmlWebpackPlugin.options.title %></title>
  9. </head>
  10.  
  11. <body>
  12. <div class="main-head">
  13. <%= require('@/common/components/header/header.ejs')() %>
  14. </div>
  15.  
  16. <div class="main-content">
  17. <%= htmlWebpackPlugin.options.content %>
  18. </div>
  19.  
  20. <div class="main-foot">
  21. <%= require('@/common/components/footer/footer.ejs')() %>
  22. </div>
  23. </body>
  24.  
  25. </html>
  26.  

2、公共页面

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

3、页面各自部分

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

  1. // pageA/index.html
  2. <div>
  3. <h1>pageA index</h1>
  4. </div>
  5.  
  6. // pageA/login.html
  7. <div>
  8. <%= require('@/common/components/form.ejs')() %>
  9. <h1>pageA login</h1>
  10. </div>
  11.  

网上查了很多资料,没找到可以实现上面步骤的方法,基本都是要在每个页面的js里去写一些ejs语法,做不到我想要的只关注此页面本身的内容。

替换 _main.ejs,生成临时模板

我的解决方法是 通过 node 读取页面 html 文件,然后替换 _main.ejs 中的 content 部分,生成一个临时 ejs 模板文件,然后通过插件 html-webpack-plugin 生成最终页面 html 文件

  1. function createTemplate(file, jsPath, entry) {
  2. let obj = {
  3. title: '',
  4. template: '',
  5. filename: '',
  6. chunks: [jsPath]
  7. }
  8. // _main.ejs 页面主题框架,html组件化
  9. let mainHtml = path.resolve(__dirname, '../src/_main.ejs')
  10. let fileSplit = file.split('/')
  11. // html 生成路径
  12. let filename = fileSplit.slice(fileSplit.length - 2).join('/').split('.')[0];
  13.  
  14. let strContent = fs.readFileSync(file, 'utf-8')
  15. let strMain = fs.readFileSync(mainHtml, 'utf-8')
  16. let template = fileSplit.slice(fileSplit.length - 2).join('_').split('.')[0];
  17. strMain = strMain.replace(/<%= htmlWebpackPlugin.options.content %>/, strContent)
  18. fs.writeFileSync(path.resolve(__dirname, `../src/template/template_${template}.ejs`), strMain)
  19.  
  20. obj.template = path.resolve(__dirname, `../src/template/template_${template}.ejs`)
  21. obj.filename = filename
  22. return obj
  23. }
  24.  

有了上面方法的思路,我们可以在各自页面中做更多的操作

页面 title

  1. // pageA/index.html
  2.  
  3. <%=title 页面A %>
  4. <div>
  5. <h1>pageA index</h1>
  6. </div>
  7.  

页面直接引入js,只压缩不打包

  1. // pageA/index.html
  2.  
  3. <%=title 页面A %>
  4.  
  5. <div>
  6. <h1>pageA index</h1>
  7. </div>
  8.  
  9. <script src="js/common/util.js"></script>
  10. <script src="js/common/server.api.js"></script>
  11.  

这里引入js的路径是最终文件压缩生成的位置(dist目录下),因为开发模式和生产环境路径有所不同,所以等下在代码中要区别不同环境去替换不同的路径。

页面引入ejs组件

  1. // pageA/index.html
  2.  
  3. <%=title 页面A %>
  4.  
  5. <div>
  6. <%= require('@/common/components/form.ejs')() %>
  7. <h1>pageA index</h1>
  8. </div>
  9.  
  10. <script src="js/common/util.js"></script>
  11. <script src="js/common/server.api.js"></script>
  12.  

page.config.js

  1. const fs = require('fs')
  2. const path = require('path')
  3. const glob = require('glob')
  4.  
  5. if (process.env.NODE_ENV === 'development') {
  6. const rimraf = require('rimraf')
  7. rimraf.sync(path.resolve(__dirname, '../src/template/*'), fs, function cb() {
  8. console.log('template目录已清空')
  9. })
  10. }
  11.  
  12. const pages = (entries => {
  13. let entry = {}, htmlArr = []
  14. // 格式化生成入口
  15. entries.forEach((file) => {
  16. // ...../webpack-mvc/src/page/pageA/index.html
  17. let fileSplit = file.split('/')
  18. let length = fileSplit.length
  19.  
  20. // 页面入口 page/pageA/index.html
  21. let filePath = fileSplit.slice(length - 3, length).join('/')
  22.  
  23. // 根据html路径找到对应的js路径,js可以和html放在同一文件夹,也可单独放在一个文件夹内,只要能找到
  24. let jsFile = path.resolve(__dirname, `../src/${filePath.split('.')[0]}.js`)
  25. if (!fs.existsSync(jsFile)) {
  26. return;
  27. }
  28. let jsPath = 'js/' + filePath.split('.')[0]
  29. entry['js/' + filePath.split('.')[0]] = jsFile
  30. htmlArr.push(createTemplate(file, jsPath, entry))
  31. })
  32. return {entry, htmlArr}
  33. })(glob(path.resolve(__dirname, '../src/page/*/*.html'), {sync: true}))
  34.  
  35. function scriptLinkEntry(entry, file) {
  36. // file: /js/common/js/util.js
  37. let fileNew = './src/' + file.split('/').slice(2).join('/')
  38. let fileSplit = fileNew.split('/')
  39. entry['js/common/' + fileSplit.slice(fileSplit.length - 1).join('/').replace('.js', '')] = fileNew
  40. }
  41.  
  42. function replaceScript(content, entry) {
  43. let scriptLink = content.match(/<script.*src=["|'](.*)["|']><\/script>/g)
  44. if (scriptLink) {
  45. scriptLink.forEach(item => {
  46. // src: /js/common/js/util.js
  47. let src = item.match(/src=["|'](.*)["|']/)[1];
  48. scriptLinkEntry(entry, src)
  49. let scriptlinNew = src
  50. // 生产环境根据页面路径找到js的相对路径,开发环境 /js/ 指向 dist 目录下 js 文件夹
  51. if (process.env.NODE_ENV === 'production') {
  52. let srcSplit = src.split('/')
  53. srcSplit.splice(3, 1) // ['', 'js', 'common', 'util.js']
  54. scriptLinkNew = `..${srcSplit.join('/')}` // ../js/common/util.js
  55. }
  56. content = content.replace(src, scriptLinkNew)
  57. })
  58. }
  59. return content;
  60. }
  61.  
  62. function createTemplate(file, jsPath, entry) {
  63. let obj = {
  64. title: '',
  65. template: '',
  66. filename: '',
  67. chunks: [jsPath]
  68. }
  69. // _main.ejs 页面主题框架,html组件化
  70. let mainHtml = path.resolve(__dirname, '../src/_main.ejs')
  71. let fileSplit = file.split('/')
  72. // html 生成路径
  73. let filename = fileSplit.slice(fileSplit.length - 2).join('/').split('.')[0];
  74.  
  75. let strContent = fs.readFileSync(file, 'utf-8')
  76. let strMain = fs.readFileSync(mainHtml, 'utf-8')
  77. let template = fileSplit.slice(fileSplit.length - 2).join('_').split('.')[0]
  78.  
  79. // 提取页面title
  80. let titleMatch = strContent.match(/<%=title(.*)%>/)
  81. let title = ''
  82. if (titleMatch) {
  83. title = titleMatch[1]
  84. strContent = strContent.replace(/<%=title(.*)%>/, '')
  85. }
  86.  
  87. // 提取页面与主体框架中引入的静态js文件,将其放入入口文件中经行压缩,并适应开发与生产路径
  88. strMain = replaceScript(strMain, entry)
  89. strContent = replaceScript(strContent, entry)
  90.  
  91. strMain = strMain.replace(/<%= htmlWebpackPlugin.options.content %>/, strContent)
  92. fs.writeFileSync(path.resolve(__dirname, `../src/template/template_${template}.ejs`), strMain)
  93.  
  94. obj.title = title
  95. obj.template = path.resolve(__dirname, `../src/template/template_${template}.ejs`)
  96. obj.filename = filename
  97. return obj
  98. }
  99.  
  100. module.exports = pages;
  101.  

热刷新

此时热刷新只能监听到js和css的改变,因为模板是动态生成的,更改页面内容时模板并没有改变,所以无法触发devServer的热刷新,手动刷新也不会有变化,因为临时模板文件没有改变,借用插件 watch 来监听html文件变化,然后重写模板文件可解决问题。

  1. const fs = require('fs')
  2. const path = require('path')
  3. const watch = require('watch')
  4. const { replaceScript } = require('./page.config.js')
  5.  
  6. watch.watchTree(path.resolve(__dirname, '../src/page'), (f, curr, prev) => {
  7. if (typeof f == 'object' && prev === null && curr === null) {
  8. // Finished walking the tree
  9. } else if (prev === null) {
  10. // f is a new file
  11. createTemplate(f)
  12. } else if (curr.link === 0) {
  13. // f was removed
  14. } else {
  15. createTemplate(f)
  16. }
  17. })
  18.  
  19. function createTemplate(file) {
  20. if (file.indexOf('.html') === -1) {
  21. return
  22. }
  23. console.log('file', file)
  24. let mainHtml = path.resolve(__dirname, '../src/_main.ejs')
  25. let strContent = fs.readFileSync(file, 'utf-8')
  26. let strMain = fs.readFileSync(mainHtml, 'utf-8')
  27. let template = file.split('\\').slice(file.split('\\').length - 2).join('_').split('.')[0]
  28. // 提取页面与主体框架中引入的静态js文件,将其放入入口文件中经行压缩,并适应开发与生产路径
  29. // 这里不再处理 title 和 静态js 入口压缩
  30. strMain = replaceScript(strMain, {}, true)
  31. strContent = replaceScript(strContent, {}, true)
  32. strContent = strContent.replace(/<%=(.*)%>/, '')
  33. strMain = strMain.replace(/<%= htmlWebpackPlugin.options.content %>/, strContent)
  34. fs.writeFileSync(path.resolve(__dirname, `../src/template/template_${template}.ejs`), strMain)
  35. }
  36.  

这里不再处理title和静态js入口压缩,更改了这些只能再重新 npm run dev

国际化

  1. const languageProperty = require('../properties/language.properties.js')
  2.  
  3. function getLanText(val) {
  4. let lan = 'zh' // $.cookie('lan')
  5. let str = languageProperty[val] && languageProperty[val][lan] || val
  6. let defaultOpt = languageProperty[val] && languageProperty[val]['default']
  7. let opts = defaultOpt && $.extend(true, [], defaultOpt)
  8. opts ? opts.unshift('') : false
  9. let args = opts && arguments.length === 1 ? opts : arguments
  10. if (args.length > 1) {
  11. let params = Array.property.slice.call(args, 1)
  12. return str.replace(/{(\d+)}/g, function(curr, index) {
  13. return params[index]
  14. })
  15. } else {
  16. return str
  17. }
  18. }
  19.  
  20. function translateAll() {
  21. let num = $('html').find('[lang]').length
  22. let count = 0
  23. if (num === 0) {
  24. $('body').show()
  25. }
  26. $('html').find('[lang]').each(function() {
  27. count += 1;
  28. let lang = $(this).attr('lang')
  29. if (lang === '') {
  30. return;
  31. }
  32. let nodeName = $(this)[0].nodeName
  33. let text = getLanText(lang)
  34. // 简单处理,复杂的可再这里更改
  35. if (nodeName === 'INPUT') {
  36. $(this).attr('placeholder', text)
  37. } else {
  38. $(this).html(text)
  39. }
  40. if (count === num) {
  41. $('body').show()
  42. }
  43. })
  44. }
  45.  
  46. module.exports = { getLanText, translateAll }
  47.  

在header.js里调用一次就可以了。

结语

至此,传统多页面组件化开发流程基本完成,可以完全脱离后台愉快的开发前端了,抛弃eclipse,拥抱vsCode。

此文只构建了基本的框架,中间还有很多优化点,打包速度,公共代码等等都没有去细究,等页面、代码量增加,这也是必须去研究的,路漫漫其修远兮。

Guthub 可直接 npm run dev, npm run build 运行, 顺便求个Star 😄

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

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号