多頁架構的先後端分離方案(webpack+express)

SPA(單頁架構)方案當下雖然很時髦,不過大多數的網站依舊選擇多頁或者單頁+多頁的混合架構。使用 express, webpack 本文低成本的實現了包含多頁架構自動刷新先後端分離 等概念javascript

先上項目

  1. git repo
    node-pages-webpack-hotcss

  2. 開發html

npm install
 npm install supervisor -g
 npm run start # 開發環境,配置 hot reload
 npm run prod # 生產環境
 npm run build # 編譯前端生產環境
  1. DEMO
    ezgif-2-dec6b379f7.gif前端

  2. FE目錄:
    Paste_Image.pngjava

  3. SERVER目錄:
    Paste_Image.pngnode

爲了避免浪費你的時間,在閱讀如下內容時須要有:webpack

  • express 基礎知識,以及對 node 簡單瞭解nginx

  • webpack 中級瞭解,本文采用 webpack2 實現git

1. FE 端配置

前端配置須要實現的功能點:github

  • 多頁架構自動生成 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 分別注入到:
<!--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 服務,實現的功能點以下:

  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 基礎使用,參閱文檔便可

2.4 代理後端接口

dev環境時使用 http-proxy-middleware 對後端接口進行代理:

// set proxy
  app.use('/api', proxy({target: config.proxy, changeOrigin: true}));

全部 /api 的請求都會代理到 config.proxy 配置的 ip 端口。

在正式環境中直接配置 nginx 進行轉發

補充

本文拋磚引玉簡單搭建了一個先後端分離框架,但還有不少不完善的地方。真實的線上應用還須要考慮 nodejs 運維成本,日誌,監控等等。

相關文章
相關標籤/搜索