脚手架包含egg、webpack、eslint、babel、happypack、sass、vue、lint-staged、热更新等特性。提供webpack构建的可配置化,扩展灵活,使用简单。css
egg是一款优秀的企业级node框架,比较经常使用的使用场景是:1.用来作BFF层,2.用来作全栈应用,3.作服务器端渲染页面,SEO优化。理论上它属于服务器端的开发,浏览器端的代码仍是须要有一套机制来进行组织,这样咱们先后端开发起来才能比较好的进行融合,如html、css、js这些是跟egg无关的,咱们可使用webpack来对它进行模块打包。在使用了一段时间后,总结了一些问题以下:html
思路的出发点是解决这些主要问题,固然还会有一些细节上的问题,当逐一解决后,咱们也就基本实现了这个脚手架。前端
github: egg-multiple-page-example 欢迎starvue
先看下脚手架的目录结构,目录结合注释阅读,更容易理解。node
egg-multiple-page-example
|
├─app.js egg启动文件,能够在应用启动的时候作点事情
|
│
├─app 项目目录,主要存放node端的代码,跟常规的egg目录结构基本一致,具体参考egg的官方文档
│ │ router.js 路由总入口
│ │
│ ├─controller 控制器模块
│ │ └─example 每一个模块一个目录,模块下面还能够分目录
│ │ detail.js 一个页面一个js,里面包含有页面渲染和http接口的逻辑代码
│ │ home.js
│ │ vue.js
│ │
│ ├─extend 自定义扩展模块
│ │ application.js
│ │ context.js
│ │ helper.js
│ │ request.js
│ │ response.js
│ │
│ ├─middleware 中间件模块
│ │ errorHandler.js
│ │
│ ├─router 每一个模块的路由配置,一个模块一个文件
│ │ example.js
│ │
│ └─service 后端服务模块,一个模块一个文件,里面是该模块下后端接口服务
│ music.js
│
├─build webpack的配置目录
│ │ build.js
│ │ config.js webpack的可配置文件,能够在这里进行一些自定义的配置,简化配置
│ │ devServer.js
│ │ hotReload.js
│ │ utils.js
│ │ webpack.base.conf.js
│ │ webpack.dev.conf.js
│ │ webpack.prd.conf.js
│ │
│ ├─loaders 自定义webpack loaders
│ │ hot-reload-loader.js
│ │
│ └─plugins 自定义webpack plugins
│ compile-html-plugin.js
│
├─config egg的配置文件,分环境配置
│ client.config.js
│ config.default.js
│ config.dev.js
│ config.local.js
│ config.prod.js
│ config.test.js
│ plugin.js
│
├─dist webpack构建生产环境存放的文件目录
│
├─temp 本地开发时的临时目录,存放编译后的html文件
│
└─src 浏览器端的文件目录
├─assets 纯静态资源目录,如一些doc、excel、示例图片等,构建时会复制到dist/static目录下
├─common 公共模块,如公共的css和js,可自定义添加
│ ├─css 公共样式
│ │ common.scss
│ │
│ └─js 公共js
│ initRun.js 页面初始化执行的代码块,如有初始化执行的方法可放于此
│ regex.js 统一正则管理
│ utils.js 前端工具方法
│
├─images 图片目录,一个模块一个目录
│ │ favicon.ico
│ │
│ ├─common 公共图片,目录下面的图片不会转成base64,也不会添加md5,用于可复用的图片和对外提供的图片
│ └─example 各个模块下面的图片,小图片会转成base64
│ vue-logo.png
│
└─templates 业务代码目录,存放每一个页面和组件的代码,components为保留目录
├─components 自定义组件的目录,vue组件放在vue目录下
│ ├─footer 若是组件包括html、js、css必需要用目录包起来,并且文件名要跟目录名一致
│ │ footer.html
│ │ footer.scss
│ │
│ ├─header 若是组件只是html,能够直接html文件便可,这种通常是nunjucks模板
│ │ header.html
│ │
│ └─vue vue组件的专用目录
│ helloWorld.vue
│
└─example 各个模块的目录,目录下面还能够再分子目录
├─detail 一个目录一个页面,分别包含html、css、js文件,命名跟目录名一致
│ detail.html
│ detail.js
│ detail.scss
│
├─home
│ home.html
│ home.js
│ home.scss
│
└─vue
app.vue
vue.html
vue.js
vue.scss
复制代码
上面是脚手架的一个先后端代码流向图,开发时,咱们须要启动webpack和egg两个服务,webpack进程用来编译html、css、js等代码,其中html会写到本地的一个temp目录,让egg能够直接读取html模板,css和js会挂载到express服务器上,这样咱们就能够经过http的方式来访问css和js代码了。但是这样会出现一个问题,就是egg和express两个服务器是不一样端口的,而咱们真正访问的页面是在egg上,express用来提供css和js的,而页面上引入css和js是用相对路径的,而不是express服务器上的路径,直接就404了,同时,也会致使热更新失败,由于跨域了。react
这时,咱们能够利用nginx来作反向代理,主服务器统一用的nginx,而后经过nginx来代理egg和express,把两个服务器打通,并解决跨域的问题。这样就解决上面提到的问题1,下面给出nginx的配置:webpack
server {
listen 80;
server_name local.example.com;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:7113/;
}
#开发环境下使用,生产环境须要注释
location /static/ {
proxy_pass http://127.0.0.1:7213/static/;
}
}
复制代码
熟悉react或者vue开发的同窗应该不陌生,让咱们开发单页面应用时,js或者css代码修改后,会自动通知浏览器更新模块代码,而且不会刷新浏览器,整个开发过程是很是顺畅。但是在多页面时,若是更新呢?在webpack里,热更新是经过HotModuleReplacementPlugin
和module.hot.accept
方法结合才能够达到热更新的效果。最简单的方法就是在入口文件添加以下代码:nginx
if (module.hot) {
module.hot.accept();
}
复制代码
这样子模块或者自身模块代码更新了,webpack就会通知浏览器。
那多页面的状况其实也简单啦,也就是在每一个页面的主js添加这段代码就能够了嘛...但是这样会不会有点傻,若是有50个页面就有50段这样的代码...囧。这里我想到一个方法,能够借助自定义loader,在每一个js编译的时候自动加上这段代码不就能够了嘛。git
// hot-reload-loader.js
module.exports = function (source) {
// 在js源代码后面添加热更新代码
let result = source + ` if (module.hot) { module.hot.accept(); } `;
return result;
};
复制代码
// webpack.base.conf.js
// 开发环境,给js添加HMR代码
...
{
test: /\.js$/,
loaders: devMode && reload ? [].concat(['hot-reload-loader']) : [],
include: [path.join(__dirname, '../src/templates')]
},
...
复制代码
这时我又遇到了一个问题,一开始我使用的是htmlWebpackPlugin
这个插件来编译html,主要是给html自动注入css和js,当页面愈来愈多的时候,html的编译就会愈来愈慢,后来我在html插件里面进行打印标记输出,一个页面的修改,会触发全部页面的编译,怪不得那么慢了。在网上扒了好久都没有找到解决方案,好吧,那就本身动手解决吧。
首先咱们要作的是注入css和js,并且要一个页面的修改,不会触发全部页面的从新编译。 咱们能够经过把html当作一个入口文件(像js文件那样),这样咱们就可以让webpack来监听html文件。github
// utils.js
/** * 初始化entry文件 * @param globPath 遍历的文件路径 * @returns {{}} webpack entry入口对象 */
initEntries (globPath) {
let files = glob.sync(globPath);
let entries = {};
files.forEach(function (file) {
let ext = path.extname(file);
/* 只需获取templates下面的目录 */
let entryKey = file.split('/templates/')[1].split('.')[0];
if (ext === '.js') {
/* 组件不须要添加initRun.js */
if (!(file.includes('/templates/components/'))) {
entries[entryKey] = ['./src/common/js/initRun.js', file];
} else {
entries[entryKey] = file;
}
} else {
entries[entryKey + ext] = file;
}
});
return entries;
}
复制代码
而后再webpack里到全部的html、js都做为entry文件:
// webpack.base.conf.js
const webpackConfig = {
entry: utils.initEntries('./src/templates/**/*.{js,html,nj}}'),
output: {
path: outputPath,
filename: 'js/[name].js',
publicPath: publicPath
},
...
复制代码
这样html就被webpack当作一个js文件,而通过个人一番研究,只要这个js文件进行自执行,它会的返回结果就是一串html的代码,并且里面的图片和静态资源都会自动编译为正确的路径(或者base64),这里发挥做用的是html-loader,会把html里面的img等标签进行编译。
接下来就是要解决如何插入css和js标签了。咱们能够利用webpack的compiler和compilation的hooks钩子函数,在html模块编译完之后就能够对它插入css和js。为此我作了一个webpack插件:
// compile-html-plugin.js
/** * 自定义webpack插件,用于优化多页面html的编译的。 * 为何要编写这个插件: * htmlWebpackPlugin在多页面的状况下,一个页面的修改,会触发全部页面的编译(dev环境下),一旦项目的页面超过必定量(几十个吧)就会变得很是慢。 * 使用该插件替换htmlWebpackPlugin不会触发全部页面的编译,只会编译你当前修改的页面,所以速度是很是快的,而且写入到temp目录。 * 插件主要使用到自定义webpack plugin的一些事件和方法,具体能够参考文档: * https://doc.webpack-china.org/api/plugins/compiler * https://doc.webpack-china.org/api/plugins/compilation */
'use strict';
const vm = require('vm');
const fs = require('fs');
const _ = require('lodash');
const mkdirp = require('mkdirp');
const config = require('../config');
class CompileHtmlPlugin {
constructor (options) {
this.options = options || {};
}
// 将 `apply` 定义为其原型方法,此方法以 compiler 做为参数
apply (compiler) {
const self = this;
self.isInit = false; // 是否已经第一次初始化编译了
self.rawRequest = null; // 记录当前修改的html路径,单次编译html会用到
/** * webpack4的插件添加compilation钩子方法附加到CompileHtmlPlugin插件上 */
compiler.hooks.compilation.tap('CompileHtmlPlugin', (compilation) => {
/* 单次编译模块时会执行,试了不少方法,就只有这个方法可以监听单次文件的编译 */
compilation.hooks.succeedModule.tap('CompileHtmlPlugin', function (module) {
/* module.rawRequest属性能够获取到当前模块的路径,而且只有html和nj文件才进行编译 */
if (self.isInit && module.rawRequest && /^\.\/src\/templates(.+)\.(html|nj)$/g.test(module.rawRequest)) {
console.log('build module');
self.rawRequest = module.rawRequest;
}
});
});
/** * 编译完成后,在发送资源到输出目录以前 */
compiler.hooks.emit.tapAsync('CompileHtmlPlugin', (compilation, cb) => {
/* webpack首次执行 */
if (!self.isInit) {
/* 遍历全部的entry入口文件 */
_.each(compilation.assets, function (asset, key) {
if (/\.(html|nj)\.js$/.test(key)) {
const filePath = key.replace('.js', '').replace('js/', 'temp/');
const dirname = filePath.substr(0, filePath.lastIndexOf('/'));
const source = asset.source();
self.compileCode(compilation, source).then(function (result) {
self.insertAssetsAndWriteFiles(key, result, dirname, filePath);
});
}
});
/* 单次修改html执行 */
} else {
/* rawRequest不为空,则代表此次修改的是html,能够执行编译 */
if (self.rawRequest) {
const assetKey = self.rawRequest.replace('./src/templates', 'js') + '.js';
console.log(assetKey);
const filePath = assetKey.replace('.js', '').replace('js/', 'temp/');
const dirname = filePath.substr(0, filePath.lastIndexOf('/'));
/* 获取当前的entry */
const source = compilation.assets[assetKey].source();
self.compileCode(compilation, source).then(function (result) {
self.insertAssetsAndWriteFiles(assetKey, result, dirname, filePath, true);
});
}
}
cb();
});
/** * 编译完成,进行一些属性的重置 */
compiler.hooks.done.tap('CompileHtmlPlugin', (compilation) => {
if (!self.isInit) {
self.isInit = true;
}
self.rawRequest = null;
});
}
/** * 用于把require进来的*.html.js进行沙箱执行,获取运行之后返回的html字符串 * 使用vm模块,在V8虚拟机上下文中提供了编译和运行代码的API * @param compilation webpack compilation 对象 * @param source 源代码 * @returns {*} */
compileCode (compilation, source) {
if (!source) {
return Promise.reject(new Error('请输入source'));
}
/* 定义vm的运行上下文,就是一些全局变量 */
const vmContext = vm.createContext(_.extend({ require: require }, global));
const vmScript = new vm.Script(source, {});
// 编译后的代码
let newSource;
try {
/* newSouce就是在沙箱执行js后返回的结果,这里用于获取编译后的html字符串 */
newSource = vmScript.runInContext(vmContext);
return Promise.resolve(newSource);
} catch (e) {
console.log('-------------compileCode error', e);
return Promise.reject(e);
}
}
/** * 把js和css插入到html模板,并写入到temp目录里面 * @param assetKey 当前的html在entry对象中的key * @param result html的模板字符串 * @param dirname 写入的目录 * @param filePath 写入的文件路径 * @param isReload 是否须要通知浏览器刷新页面,前提是使用插件时必须传入hotMiddleware */
insertAssetsAndWriteFiles (assetKey, result, dirname, filePath, isReload) {
let self = this;
let styleTag = `<link href="${config.publicPath}css/${assetKey.replace('.html.js', '.css').replace('js/', '')}" rel="stylesheet" />`;
let scriptTag = `<script src="${config.publicPath}${assetKey.replace('.html.js', '.js')}"></script>`;
result = result.replace('</head>', `${styleTag}</head>`);
result = result.replace('</body>', `${scriptTag}</body>`);
mkdirp(dirname, function (err) {
if (err) {
console.error(err);
} else {
fs.writeFile(filePath, result, function (err) {
if (err) {
console.error(err);
}
// 通知浏览器更新
if (isReload) {
self.options.hotMiddleware && self.options.hotMiddleware.publish({ action: 'reload' });
}
});
}
});
}
}
module.exports = CompileHtmlPlugin;
复制代码
代码不算复杂,关键的几个点就是:
<link>
和<script>
标签;这样就解决了html的编译问题了。
下面来解决问题3。问题3其实不是很难,关键是要分析出咱们的需求,咱们其实最须要的是vue的数据驱动,数据绑定还有组件的功能便可,上层工具,如vue-router、vuex、vue-cli这些其实都不是必须的,这些主要在作vue的单页应用或者ssr时才会排上用场。幸运的是vue是一个渐进式的框架,咱们能够单纯引入vue.js便可。
在webpack里单纯引入vue,实际上是比较简单的,主要用到VueLoaderPlugin
和vue-loader
便可:
// webpack.base.conf.js
...
const VueLoaderPlugin = require('vue-loader/lib/plugin');
...
module: {
rules: [
// 使用vue-loader将vue文件编译转换为js
{
test: /\.vue$/,
loader: 'vue-loader'
},
]
}
...
plugins: [
new VueLoaderPlugin(),
...
]
复制代码
就是这么简单,咱们就把vue引进咱们的项目里,并非全部vue项目都须要vue-cli哦。
在项目中使用vue,咱们还能够利用一些技巧来提高咱们的页面加载速度,如懒加载,下面是几种加载方式的例子:
// 传统的同步加载
import Vue from 'vue';
import app from './app.vue';
new Vue({
el: '#app',
render: h => h(app)
});
// 按顺序异步加载js
import('vue').then(async ({ default: Vue }) => {
const { default: app } = await import('./app.vue');
new Vue({
el: '#app',
render: h => h(app)
});
});
// 多个异步js同时加载
Promise.all([
// 打包时给异步的js添加命名
import(/* webpackChunkName: 'async' */ 'vue'),
import('./app.vue')
]).then(([{ default: Vue }, { default: app }]) => {
new Vue({
el: '#app',
render: h => h(app)
});
});
复制代码
还有一点要注意的是你挂载到html中的根节点必需要和vue根节点的id(固然,你能够用class也行)是同样的,如#app
,否则热更新的时候会找不到元素挂载,报错。
项目中还用到了一些webpack性能优化和公共代码抽取等,如happypack
、OptimizeCSSPlugin
、splitChunks
等,这些都有现成的官方文档,这里就不作讲解了。
若是有地方不明白能够在下面留言或者上github提issue,若是项目对你帮助,请给我个star吧。传送门