React最佳实践尝试(二)

栏目: JavaScript · 发布时间: 6年前

内容简介:首先我的整体思路是:根据webpack.ssr.config.js配置文件,将前端代码打包进node层供node做SSR使用,然后前端正常启动webpack-dev-server服务器即可。将前端代码打包进node之后,在正常启动node服务器即可:这样基本上webpack整体的打包思路就清晰了。

首先我的整体思路是:根据webpack.ssr.config.js配置文件,将前端代码打包进node层供node做SSR使用,然后前端正常启动webpack-dev-server服务器即可。

package.json

"startfe": "run-p client ssr",
"client": "BABEL_ENV=client NODE_ENV=development webpack-dev-server --config public/webpack.dev.config.js",
"ssr": "BABEL_ENV=ssr NODE_ENV=development webpack --watch --config public/webpack.ssr.config.js",
复制代码

将前端代码打包进node之后,在正常启动node服务器即可:

package.json

"start": "BABEL_ENV=server NODE_ENV=development nodemon src/app.ts --exec babel-node --extensions '.ts,.tsx'",
复制代码

这样基本上webpack整体的打包思路就清晰了。

最终生产模式中,我们只需要将整个前端代码通过webpack打包进 src 目录,然后将整个 src 目录经过babel转义之后输出到 output 目录,最终我们的生产模式只需要启动 output/app.js 即可。

package.json

"buildfe": "run-p client:prod ssr:prod",
"build": "BABEL_ENV=server NODE_ENV=production babel src -D -d output/src --extensions '.ts,.tsx'",
"ssr:prod": "BABEL_ENV=ssr NODE_ENV=production webpack --config public/webpack.ssr.config.js",
"client:prod": "BABEL_ENV=client NODE_ENV=production webpack --progess --config public/webpack.prod.config.js",
复制代码
$ node output/app.js // 启动生产模式
复制代码

webpack配置

在客户端的打包中,我们需要使用 webpack-manifest-plugin 插件。这个插件可以将webpack打包之后所有文件的路径写入一个 manifest.json 的文件中,我们只需要读取这个文件就可以找到所有资源的正确路径了。

部分webpack.client.config.js

const ManifestPlugin = require("webpack-manifest-plugin");
module.exports = merge(baseConfig, {
  // ...
  plugins: [
    new ManifestPlugin(),
    // ...
  ]
});
复制代码

Mapping loaded modules to bundles

In order to make sure that the client loads all the modules that were rendered server-side, we'll need to map them to the bundles that Webpack created.

我们的客户端渲染使用了 react-loadable ,需要知道该模块是否提前经过了服务端渲染,否则会出现重复加载的问题。因此需要将webpack打包后的 bundles 生成一个map文件,然后在ssr的时候传入 react-loadable 。这里我们使用 react-loadable/webpack 插件即可。

部分webpack.client.config.js

import { ReactLoadablePlugin } from 'react-loadable/webpack';
const outputDir = path.resolve(__dirname, "../src/public/buildPublic");
plugins: [
    // ...
    new ReactLoadablePlugin({
      filename: path.resolve(outputDir, "react-loadable.json")
    })
    // ...
  ],
复制代码

接下来是webpack打包产物的资源路径问题。

生产模式一般都是将输出的文件上传到cdn上,因此我们只需要在pubicPath的地方使用cdn地址即可。

部分webpack.prod.config.js

mode: "production",
output: {
    filename: "[name].[chunkhash].js",
    publicPath: "//cdn.address.com",
    chunkFilename: "chunk.[name].[chunkhash].js"
  },
复制代码

开发环境中我们只需要读取 manifest.json 文件中相对应模块的地址即可。

manifest.json

{
  "home.js": "http://127.0.0.1:4999/static/home.js",
  "home.css": "http://127.0.0.1:4999/static/home.css",
  "home.js.map": "http://127.0.0.1:4999/static/home.js.map",
  "home.css.map": "http://127.0.0.1:4999/static/home.css.map"
}
复制代码

SSR代码

解决了打包问题之后,我们需要考虑ssr的问题了。

其实整体思路比较简单:我们通过打包,已经有了 manifest.json 文件储存静态资源路径,有 react-loadable.json 文件储存打包输出的各个模块的信息,只需要在ssr的地方读出js、css路径,然后将被 <Loadable.Capture /> 包裹的组件 renderToString 一下,填入pug模板中即可。

src/utils/bundle.ts

function getScript(src) {
  return `<script type="text/javascript" src="${src}"></script>`;
}
function getStyle(src) {
  return `<link rel="stylesheet" href="${src}" />`;
}

export { getScript, getStyle };
复制代码

src/utils/getPage.ts

import { getBundles } from "react-loadable/webpack";
import React from "react";
import { getScript, getStyle } from "./bundle";
import { renderToString } from "react-dom/server";
import Loadable from "react-loadable";

export default async function getPage({
  store,
  url,
  Component,
  page
}) {
  const manifest = require("../public/buildPublic/manifest.json");
  const mainjs = getScript(manifest[`${page}.js`]);
  const maincss = getStyle(manifest[`${page}.css`]);

  let modules: string[] = [];

  const dom = (
    <Loadable.Capture
      report={moduleName => {
        modules.push(moduleName);
      }}
    >
      <Component url={url} store={store} />
    </Loadable.Capture>
  );
  const html = renderToString(dom);

  const stats = require("../public/buildPublic/react-loadable.json");
  let bundles: any[] = getBundles(stats, modules);

  const _styles = bundles
    .filter(bundle => bundle && bundle.file.endsWith(".css"))
    .map(bundle => getStyle(bundle.publicPath))
    .concat(maincss);
  const styles = [...new Set(_styles)].join("\n");

  const _scripts = bundles
    .filter(bundle => bundle && bundle.file.endsWith(".js"))
    .map(bundle => getScript(bundle.publicPath))
    .concat(mainjs);
  const scripts = [...new Set(_scripts)].join("\n");

  return {
    html,
    __INIT_STATES__: JSON.stringify(store.getState()),
    scripts,
    styles
  };
}
复制代码

路径说明: src/public 目录存放所有前端打包过来的文件, src/public/buildPublic 存放 webpack.client.config.js 打包的前端代码, src/public/buildServer 存放 webpack.ssr.config.js 打包的服务端渲染的代码。

这样服务端渲染的部分就基本完成了。

其他node层启动代码可以直接查看 src/server.ts 文件即可。

前后端同构

接下来就要编写前端的业务代码来测试一下服务端渲染是否生效。

这里我们要保证使用最少的代码完成前后端同构的功能。

首先我们需要在webpack中定义个变量 IS_NODE ,在代码中根据这个变量就可以区分ssr部分的代码和客户端部分的代码了。

webpack.client.config.js

plugins: [
    // ...
    new webpack.DefinePlugin({
        IS_NODE: false
    })
    // ...
]
复制代码

接下来编写前端页面的入口文件,入口这里要对ssr和client做区别渲染:

public/js/decorators/entry.tsx

import React, { Component } from "react";
import { Provider } from "react-redux";
import ReactDOM from "react-dom";
import Loadable from "react-loadable";
import { BrowserRouter, StaticRouter } from "react-router-dom";

// server side render
const SSR = App =>
  class SSR extends Component<{
    store: any;
    url: string;
  }> {
    render() {
      const context = {};
      return (
        <Provider store={this.props.store} context={context}>
          <StaticRouter location={this.props.url}>
            <App />
          </StaticRouter>
        </Provider>
      );
    }
  };

// client side render
const CLIENT = configureState => Component => {
  const initStates = window.__INIT_STATES__;
  const store = configureState(initStates);
  Loadable.preloadReady().then(() => {
    ReactDOM.hydrate(
      <Provider store={store}>
        <BrowserRouter>
          <Component />
        </BrowserRouter>
      </Provider>,
      document.getElementById("root")
    );
  });
};

export default function entry(configureState) {
  return IS_NODE ? SSR : CLIENT(configureState);
}
复制代码

这里entry参数中的 configureState 是我们store的声明文件。

public/js/models/configure.ts

import { init } from "@rematch/core";
import immerPlugin from "@rematch/immer";
import * as models from "./index";

const immer = immerPlugin();

export default function configure(initStates) {
  const store = init({
    models,
    plugins: [immer]
  });
  for (const model of Object.keys(models)) {
    store.dispatch({
      type: `${model}/@init`,
      payload: initStates[model]
    });
  }
  return store;
}
复制代码

这样就万事俱备了,接下来只需要约定我们单页的入口即可。

这里我将单页的入口都统一放到 public/js/entry 目录下面,每一个单页都是一个目录,比如我的项目中只有一个单页,因此我只创建了一个 home 目录。

每一个目录下面都有一个 index.tsx 文件和一个 routes.tsx 文件,分为是单页的整体入口代码,已经路由定义代码。

例如:

/entry/home/routes.tsx

import Loadable from "react-loadable";
import * as Path from "constants/path";
import Loading from "components/loading";

export default [
  {
    name: "demo",
    path: Path.Demo,
    component: Loadable({
      loader: () => import("containers/demo"),
      loading: Loading
    }),
    exact: true
  },
  {
    name: "todolist",
    path: Path.Todolist,
    component: Loadable({
      loader: () => import("containers/todolist"),
      loading: Loading
    }),
    exact: true
  }
];
复制代码

/entry/home.index.tsx

import React, { Component } from "react";
import configureStore from "models/configure";
import entry from "decorators/entry";
import { Route } from "react-router-dom";
import Layout from "components/layout";
import routes from "./routes";

class Home extends Component {
  render() {
    return (
      <Layout>
        {routes.map(({ path, component: Component, exact = true }) => {
          return (
            <Route path={path} component={Component} key={path} exact={exact} />
          );
        })}
      </Layout>
    );
  }
}

const Entry = entry(configureStore)(Home);
export { Entry as default, Entry, configureStore };
复制代码

Layout 组件是存放所有页面的公共部分,比如Nav导航条、Footer等。

这样所有的准备工作就已经做完了,剩下的工作就只有编写组件代码以及首屏数据加载了。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

优秀网站设计

优秀网站设计

林奇 (Patrick J.Lynch)、霍顿 (Sarah Horton) / 机械工业出版社 / 2012-10-1 / 69.00元

优秀网站设计:打造有吸引力的网站(原书第3版),ISBN:9787111399599,作者:(美)Patrick J. Lynch Sarah Horton 著,李静等译一起来看看 《优秀网站设计》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具