多页架构的前后端分离方案(webpack+express)

SPA(单页架构)方案当下虽然很时髦,不过大多数的网站依旧选择多页或者单页+多页的混合架构。使用 express, webpack 本文低成本的实现了包含多页架构自动刷新前后端分离 等概念

先上项目

  1. git repo
    node-pages-webpack-hot

  2. 开发

 npm install
 npm install supervisor -g
 npm run start # 开发环境,配置 hot reload
 npm run prod # 生产环境
 npm run build # 编译前端生产环境
  1. DEMO


    ezgif-2-5c3bbb985e.gif
  2. FE目录:


    Paste_Image.png
  3. SERVER目录:


    Paste_Image.png

为了不浪费你的时间,在阅读以下内容时需要有:

  • express 基础知识,以及对 node 简单了解
  • webpack 中级了解,本文采用 webpack2 实现

1. FE 端配置

前端配置需要实现的功能点:

  • 多页架构自动生成 entry,并通过 html-webpack-plugin 生成每个页面的模板,且选择任意模板引擎需要实现 layout 模板功能(本文使用swig作为模板引擎)

  • 配置各种文件后缀的 loader

  • 使用 HotModuleReplacementPlugin 实现修改自刷新

1.1 自动分析entry

规定每个页面必须有一个同名的 js 文件作为此页面的 entry ,目录深度可变,如下图,分解为两个 entry:


Paste_Image.png

为实现自动化获取,使用了 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 分别注入到:

两个替换文案处,例如页面模板:

{% 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 服务,实现的功能点如下:

  1. 使用 webpack-dev-middleware 进行 webpack 编译

  2. 使用 webpack-hot-middleware 实现 hot reload

  3. 使用 supervisor 服务监听 node 文件改动并自动重启

  4. render 模板时将内存中的文件写入硬盘,以进行渲染

2.1 webpack 接入 express

  • 生成 webpackcompiler
  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 修改自刷新

jscss 的自刷新通过配置 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 生成的模板文件。
然而 expressrender 函数只能指定一个存在于文件系统中的模板, 即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 基础使用,参阅文档即可

补充

本文抛砖引玉简单搭建了一个前后端分离框架,但还有很多不完善的地方。真实的线上应用还需要考虑 nodejs 运维成本,日志,监控等等。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容