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等。

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


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

查看所有标签

猜你喜欢:

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

Programming in Haskell

Programming in Haskell

Graham Hutton / Cambridge University Press / 2007-1-18 / GBP 34.99

Haskell is one of the leading languages for teaching functional programming, enabling students to write simpler and cleaner code, and to learn how to structure and reason about programs. This introduc......一起来看看 《Programming in Haskell》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器