SPA(單頁架構)方案當下雖然很時髦,不過大多數的網站依舊選擇多頁或者單頁+多頁的混合架構。使用
express
,webpack
本文低成本的實現了包含多頁架構
,自動刷新
,先後端分離
等概念javascript
git repo
node-pages-webpack-hotcss
開發html
npm install npm install supervisor -g npm run start # 開發環境,配置 hot reload npm run prod # 生產環境 npm run build # 編譯前端生產環境
DEMO前端
FE目錄:java
SERVER目錄:node
爲了避免浪費你的時間,在閱讀如下內容時須要有:webpack
前端配置須要實現的功能點:github
多頁架構自動生成 entry
,並經過 html-webpack-plugin
生成每一個頁面的模板,且選擇任意模板引擎須要實現 layout
模板功能(本文使用swig
做爲模板引擎)
配置各類文件後綴的 loader
使用 HotModuleReplacementPlugin
實現修改自刷新
規定每一個頁面必須有一個同名的 js 文件做爲此頁面的 entry ,目錄深度可變,以下圖,分解爲兩個 entry:
爲實現自動化獲取,使用了 glob 獲取全部 .js
文件,並判斷是否有同名的 .html
,若是有則生成一個 entry,若是是 dev 環境則多增長 hotMiddlewareScript
模塊
// get all js files let files = glob.sync(config.src + '/**/*.js'); let srcLength = config.src.length; let entrys = {}; files.forEach(function (_file) { let file = path.parse(_file); let htmlFile = path.resolve(file.dir, file.name + '.' + config.ext); // if has same name template file, it is a entry if (fs.existsSync(htmlFile)) { let pathIndex = file.dir.indexOf(config.src); if (config.dev == 'dev') { entrys[config.staticRoot + file.dir.slice(srcLength) + '/' + file.name] = [path.resolve(_file), hotMiddlewareScript]; } else { entrys[config.staticRoot + file.dir.slice(srcLength) + '/' + file.name] = path.resolve(_file); } } }); return entrys;
html-webpack-plugin
模板生成一系列 HtmlWebpackPlugin
的要點以下:
獲取到全部的 .html
後,判斷是否有對應的 entry
文件,如有則建立 HtmlWebpackPlugin
若是頁面爲 layout 模板
,則須要多注入由 CommonsChunkPlugin
生成的 common
模塊
自動生成 HtmlWebpackPlugin
代碼以下:
let htmls = []; // get all templates let files = glob.sync(config.src + '/**/*.' + config.ext); let srcLength = config.src.length; files.forEach(function (_file) { let file = path.parse(_file); let chunks = []; let chunkName = config.staticRoot + file.dir.slice(srcLength) + '/' + file.name; // if has same name entry, create a html plugin let c = entrys[chunkName]; c && chunks.push(chunkName); // layout will contains common chunk if (file.name == config.serverLayoutName) { chunks.push(config.staticRoot + '/common'); } let plugin = new HtmlWebpackPlugin({ filename: config.templateRoot + file.dir.slice(srcLength) + '/' + file.base, template: path.resolve(file.dir, file.base), chunks: chunks, inject: false }); htmls.push(plugin); }); return htmls;
因爲引入了模板 extends
支持,需設置 inject=false
便不會自動注入 assets 文件
編寫 webpack 插件,將頁面的 js assets
, css assets
分別注入到:<!--webpack_style_placeholder-->
<!--webpack_script_placeholder-->
兩個替換文案處,例如頁面模板:
{% extends '../base/base.html' %} {% block title %}My Page{% endblock %} {% block style %}<!--webpack_style_placeholder-->{%endblock%} {% block head %} {% parent %} {% endblock %} {% block content %} <p>This is just an home page!!!</p> <div class="color-area"> clouds </div> <a href="/users/list">link page2</a> {% endblock %} {% block script %}<!--webpack_script_placeholder-->{%endblock%}
編譯後替換後爲:
{% extends '../base/base.html' %} {% block title %}My Page{% endblock %} {% block style %}<link rel="stylesheet" href="/static/page/home/home.css"/>{%endblock%} {% block head %} {% parent %} {% endblock %} {% block content %} <p>This is just an home page!!!</p> <div class="color-area"> clouds </div> <a href="/users/list">link page2</a> {% endblock %} {% block script %}<script src="/static/page/home/home.js"></script>{%endblock%}
在
dev
環境下因爲配置了webpack-hot-middleware
因此不能對 css 進行提取,不然沒法熱更新
樣式相關的 loader 配置以下:
var extractInstance = new ExtractTextPlugin('[name].css'); if (config.env == 'dev') { var stylusLoader = [ { loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'stylus-loader' } ]; var cssLoader = [ { loader: 'style-loader' }, { loader: 'css-loader' } ]; } else { var stylusLoader = extractInstance.extract(['css-loader', 'stylus-loader']); var cssLoader = extractInstance.extract(['css-loader']); }
並將全部的 loader
放到同一個文件進行維護:
var rules = [ { test: /\.styl$/, exclude: /node_modules/, use: stylusLoader }, { test: /\.css$/, exclude: /node_modules/, use: cssLoader }, { test: /\.html$/, use: { loader: 'html-loader', options: { minimize: false } } }, ...... ...... ]
對生成模板,靜態文件輸出目錄進行統一控制,便於結合各類後端架構
const port = process.env.PORT || 8080; const env = process.env.NODE_ENV || 'dev'; const CONFIG_BUILD = { env: env, ext: 'html', // tempate ext src: path.resolve(__dirname, '../src'), // source code path path: env == 'dev' ? '/' : path.resolve(__dirname, '../dist'), // base output path templateRoot: 'templates', // tempate output path staticRoot: 'static', // static output path serverLayoutName: 'base', // swig layout name , only one file publicPath: env == 'dev' ? ('http://localhost:' + port + '/') : '/' }
server
端搭建了 express 服務,實現的功能點以下:
使用 webpack-dev-middleware
進行 webpack
編譯
使用 webpack-hot-middleware
實現 hot reload
使用 supervisor
服務監聽 node
文件改動並自動重啓
render
模板時將內存中的文件寫入硬盤,以進行渲染
生成 webpack
的 compiler
var webpack = require('webpack'), webpackDevConfig = require(path.resolve(config.root, './fe/webpack.config.js')); var compiler = webpack(webpackDevConfig);
將 compiler
做爲 express
的中間件
// attach to the compiler & the server app.use(webpackDevMiddleware(compiler, { // public path should be the same with webpack config publicPath: webpackDevConfig.output.publicPath, noInfo: false, stats: { colors: true } }));
其中 publicPath
指明瞭 assets
請求的根路徑,這裏配置的是:http://localhost:8080/
hot reload
方案js,css
修改自刷新js
、css
的自刷新經過配置 webpack-hot-middleware
實現(fe 也需進行相應的配置)
// server const webpackHotMiddleware = require('webpack-hot-middleware'); app.use(webpackHotMiddleware(compiler)); // fe webpackPlugins.push( new webpack.HotModuleReplacementPlugin() );
node
修改自刷新node
文件修改經過配置 supervisor
服務實現自動刷新
安裝服務:
npm install supervisor -g
配置啓動參數:
// package.json "scripts": { "start": "cross-env NODE_ENV=dev supervisor -w server -e fe server/server.js" }
supervisor
監聽了 server 文件夾下全部的改動,改動後重啓 express服務
。
想要實現瀏覽器自動刷新,須要在 layout
模板加入以下代碼:
{% if env == 'dev' %} <script src="/reload/reload.js"></script> {% endif %}
當 webpack
做爲 express
中間件時,生成的全部文件都存在內存中,固然也包括由 html-webpack-plugin
生成的模板文件。
然而 express
的 render
函數只能指定一個存在於文件系統中的模板, 即dev
環境下 render
模板前須要將其從內存中取得並存放到文件系統中。
module.exports = (res, template) => { if (config.env == 'dev') { let filename = compiler.outputPath + template; // load template from compiler.outputFileSystem.readFile(filename, function(err, result) { let fileInfo = path.parse(path.join(config.templateRoot, filename)); mkdirp(fileInfo.dir, () => { fs.writeFileSync(path.join(config.templateRoot, filename), result); res.render(template); }); }); } else { res.render(template); } }
layout
模板的存儲須要一箇中間件:
app.use((req, res, next) => { let layoutPath = path.join(config.templateRoot, config.layoutTemplate); let filename = compiler.outputPath + config.layoutTemplate; compiler.outputFileSystem.readFile(filename, function(err, result) { let fileInfoLayout = path.parse(layoutPath); mkdirp(fileInfoLayout.dir, () => { fs.writeFileSync(layoutPath, result); next(); }); }); });
其他的均爲 express 基礎使用,參閱文檔便可
在dev
環境時使用 http-proxy-middleware
對後端接口進行代理:
// set proxy app.use('/api', proxy({target: config.proxy, changeOrigin: true}));
全部
/api
的請求都會代理到config.proxy
配置的 ip 端口。
在正式環境中直接配置 nginx
進行轉發
本文拋磚引玉簡單搭建了一個先後端分離框架,但還有不少不完善的地方。真實的線上應用還須要考慮 nodejs
運維成本,日誌,監控等等。