github地址:github.com/bbwlfx/ts-b…javascript
配置完成以後,接下來就要考慮打包啓動以及先後端同構的架構方面的問題了。css
首先個人總體思路是:根據webpack.ssr.config.js配置文件,將前端代碼打包進node層供node作SSR使用,而後前端正常啓動webpack-dev-server服務器便可。html
"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服務器便可:前端
"start": "BABEL_ENV=server NODE_ENV=development nodemon src/app.ts --exec babel-node --extensions '.ts,.tsx'",
複製代碼
這樣基本上webpack總體的打包思路就清晰了。java
最終生產模式中,咱們只須要將整個前端代碼經過webpack打包進src
目錄,而後將整個src
目錄通過babel轉義以後輸出到output
目錄,最終咱們的生產模式只須要啓動output/app.js
便可。node
"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-manifest-plugin
插件。這個插件能夠將webpack打包以後全部文件的路徑寫入一個manifest.json
的文件中,咱們只須要讀取這個文件就能夠找到全部資源的正確路徑了。react
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.webpack
咱們的客戶端渲染使用了react-loadable
,須要知道該模塊是否提早通過了服務端渲染,不然會出現重複加載的問題。所以須要將webpack打包後的bundles
生成一個map文件,而後在ssr的時候傳入react-loadable
。這裏咱們使用react-loadable/webpack
插件便可。git
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打包產物的資源路徑問題。github
生產模式通常都是將輸出的文件上傳到cdn上,所以咱們只須要在pubicPath的地方使用cdn地址便可。
mode: "production",
output: {
filename: "[name].[chunkhash].js",
publicPath: "//cdn.address.com",
chunkFilename: "chunk.[name].[chunkhash].js"
},
複製代碼
開發環境中咱們只須要讀取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的問題了。
其實總體思路比較簡單:咱們經過打包,已經有了manifest.json
文件儲存靜態資源路徑,有react-loadable.json
文件儲存打包輸出的各個模塊的信息,只須要在ssr的地方讀出js、css路徑,而後將被<Loadable.Capture />
包裹的組件renderToString
一下,填入pug模板中便可。
function getScript(src) {
return `<script type="text/javascript" src="${src}"></script>`;
}
function getStyle(src) {
return `<link rel="stylesheet" href="${src}" />`;
}
export { getScript, getStyle };
複製代碼
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部分的代碼和客戶端部分的代碼了。
plugins: [
// ...
new webpack.DefinePlugin({
IS_NODE: false
})
// ...
]
複製代碼
接下來編寫前端頁面的入口文件,入口這裏要對ssr和client作區別渲染:
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的聲明文件。
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
文件,分爲是單頁的總體入口代碼,已經路由定義代碼。
例如:
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
}
];
複製代碼
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等。
這樣全部的準備工做就已經作完了,剩下的工做就只有編寫組件代碼以及首屏數據加載了。
系列文章: