腳手架包含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吧。傳送門