SPA(单页架构)方案当下虽然很时髦,不过大多数的网站依旧选择多页或者单页+多页的混合架构。使用
express
,webpack
本文低成本的实现了包含多页架构
,自动刷新
,前后端分离
等概念
先上项目
git repo
node-pages-webpack-hot开发
npm install
npm install supervisor -g
npm run start # 开发环境,配置 hot reload
npm run prod # 生产环境
npm run build # 编译前端生产环境
DEMO
FE目录:
SERVER目录:
为了不浪费你的时间,在阅读以下内容时需要有:
1. FE 端配置
前端配置需要实现的功能点:
多页架构自动生成
entry
,并通过html-webpack-plugin
生成每个页面的模板,且选择任意模板引擎需要实现layout
模板功能(本文使用swig
作为模板引擎)配置各种文件后缀的 loader
使用
HotModuleReplacementPlugin
实现修改自刷新
1.1 自动分析entry
规定每个页面必须有一个同名的 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;
1.2 自动生成 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%}
1.3 各种 loader 配置,提取页面 css
在
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
}
}
},
......
......
]
1.4 路径配置
对生成模板,静态文件输出目录进行统一控制,便于结合各种后端架构
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 + '/') : '/'
}
2. SERVER 端配置
server
端搭建了 express 服务,实现的功能点如下:
使用
webpack-dev-middleware
进行webpack
编译使用
webpack-hot-middleware
实现hot reload
使用
supervisor
服务监听node
文件改动并自动重启render
模板时将内存中的文件写入硬盘,以进行渲染
2.1 webpack 接入 express
生成
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/
2.2 hot reload
方案
2.2.1 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()
);
2.2.2 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 %}
2.3 对 template 进行 render
当 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 基础使用,参阅文档即可
2.4 代理后端接口
在dev
环境时使用 http-proxy-middleware
对后端接口进行代理:
// set proxy
app.use('/api', proxy({target: config.proxy, changeOrigin: true}));
所有
/api
的请求都会代理到config.proxy
配置的 ip 端口。
在正式环境中直接配置 nginx
进行转发
补充
本文抛砖引玉简单搭建了一个前后端分离框架,但还有很多不完善的地方。真实的线上应用还需要考虑 nodejs
运维成本,日志,监控等等。