webpack-mvc 传统多页面组件化开发详解
最近有一个项目,还是使用的传统 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 ????
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。