server-renderer
Version:
library of server side render for React
310 lines (291 loc) • 11.7 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var http = require('http');
var path = require('path');
var path__default = _interopDefault(path);
var fs = require('fs');
var fs__default = _interopDefault(fs);
var React = require('react');
var cheerio = require('cheerio');
var url = require('url');
var pathToRegexp = require('path-to-regexp');
var ReactDOMServer = _interopDefault(require('react-dom/server'));
var H = require('history');
var reactDom = require('react-dom');
var mime = require('mime');
var util = require('util');
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
function __awaiter(thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
const getConfig = () => {
const rootDir = process.cwd();
const defaultConfig = {
rootDir,
isDev: process.env.NODE_ENV === 'development',
port: 3000,
htmlTemplatePath: path__default.join(rootDir, 'src/index.html'),
distDir: path__default.join(rootDir, 'dist'),
builtHTMLPath: path__default.join(rootDir, 'dist/client/index.html'),
serverEntry: path__default.resolve(rootDir, 'src/index.tsx'),
publicPath: '/public/',
cleanConsoleOnRebuild: true,
decodeEntities: false,
sassData: undefined,
};
const customConfigPath = path__default.resolve(defaultConfig.rootDir, 'ssr.config.js');
if (fs__default.existsSync(customConfigPath)) {
const customConfig = require(customConfigPath);
return Object.assign(Object.assign(Object.assign({}, defaultConfig), customConfig), { rootDir });
}
return defaultConfig;
};
function findMatchedRoute(url$1, routes = []) {
// 通配符
const wildcard = '(.*)';
const { pathname } = url.parse(url$1);
const matched = routes
.map(route => {
if (route.path === '*') {
return Object.assign(Object.assign({}, route), { path: wildcard });
}
return route;
})
.find(route => {
return pathToRegexp.pathToRegexp(route.path).test(pathname + '');
});
return matched;
}
function callGetInitialProps(App, Component, url, req, res) {
return __awaiter(this, void 0, void 0, function* () {
if (App.getInitialProps) {
const params = {
Component,
url,
req,
res,
};
return yield App.getInitialProps(params);
}
return {};
});
}
const RouterContext = React.createContext({});
const history = 'undefined' === typeof window
? H.createMemoryHistory()
: H.createBrowserHistory();
const useLocation = () => {
return React.useContext(RouterContext).location;
};
const useParams = () => {
return React.useContext(RouterContext).params;
};
function matchParams(path, pathname) {
const matchFn = pathToRegexp.match(path);
const matched = matchFn(pathname);
return matched ? matched.params : {};
}
const Router = props => {
const component = React.useRef(props.component);
const pageProps = React.useRef(props.pageProps);
const [location, setLocation] = React.useState(() => {
const parsedUrl = url.parse(props.url);
return {
pathname: parsedUrl.pathname || '/',
search: parsedUrl.search || '',
hash: parsedUrl.hash || '',
state: null,
};
});
const [params, setParams] = React.useState(() => {
const matchedRoute = findMatchedRoute(props.url, props.routes);
return matchParams(matchedRoute ? matchedRoute.path : '', location.pathname);
});
React.useEffect(() => {
const unlisten = history.listen((newLocation) => __awaiter(void 0, void 0, void 0, function* () {
if (newLocation.pathname === location.pathname) {
// H.locationsAreEqual(location, newLocation)
// 忽略 search 变化
return;
}
const matched = findMatchedRoute(newLocation.pathname, props.routes);
if (matched) {
const initialProps = yield callGetInitialProps(props.App, matched.component, newLocation.pathname);
pageProps.current = initialProps;
component.current = matched.component;
}
else {
component.current = null;
}
reactDom.unstable_batchedUpdates(() => {
setLocation(newLocation);
setParams(matched ? matchParams(matched.path, newLocation.pathname) : {});
});
}));
return unlisten;
}, [location.pathname]);
const store = React.useMemo(() => ({ location, params }), [location]);
return (React.createElement(RouterContext.Provider, { value: store },
React.createElement(props.App, { pageProps: pageProps.current, Component: component.current })));
};
const Root = props => {
return (React.createElement(Router, { url: props.url, routes: props.routes, component: props.component, pageProps: props.pageProps, App: props.App }));
};
const App = (_a) => {
var { Component } = _a, otherProps = __rest(_a, ["Component"]);
if (Component) {
return React.createElement(Component, Object.assign({}, otherProps));
}
return null;
};
App.getInitialProps = (params) => __awaiter(void 0, void 0, void 0, function* () {
const { Component } = params;
if (Component && Component.getInitialProps) {
return yield Component.getInitialProps(params);
}
return {};
});
const config = getConfig();
const htmlPath = config.isDev ? config.htmlTemplatePath : config.builtHTMLPath;
const template = fs.readFileSync(htmlPath, 'utf8');
function writeDataToHTML($, pageProps) {
const data = JSON.stringify({
pageProps,
});
const scripts = $('script');
if (scripts.length) {
$(scripts.get(0)).before(`
<script type="text/javascript">
window.__APP_DATA__ = ${data}
</script>
`);
}
else {
$('body').append(`<script type="text/javascript">
window.__APP_DATA__ = ${data}
</script>`);
}
}
function appendScriptOnDevelop($) {
if (config.isDev) {
const scriptUrl = config.publicPath + 'app.js';
$('body').append(`
<script type="text/javascript" src="${scriptUrl}"></script>
`);
}
}
function renderToString(req, res, url, options) {
return __awaiter(this, void 0, void 0, function* () {
const $ = cheerio.load(template, {
decodeEntities: config.decodeEntities,
});
const matched = findMatchedRoute(url, options.routes);
const App$1 = options.App || App;
const initialProps = yield callGetInitialProps(App$1, matched ? matched.component : null, url, req, res);
const content = ReactDOMServer.renderToString(React.createElement(Root, { url: url, routes: options.routes || [], component: matched ? matched.component : null, pageProps: initialProps, App: App$1 }));
$(options.container).html(content);
appendScriptOnDevelop($);
writeDataToHTML($, initialProps);
return $.html();
});
}
const config$1 = getConfig();
const existsAsync = util.promisify(fs.exists);
const statAsync = util.promisify(fs.stat);
function sendStaticFile(req, res) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const filePath = path.join(config$1.distDir, 'client', req.url.replace(config$1.publicPath, ''));
const isExists = yield existsAsync(filePath);
if (isExists) {
if (!config$1.isDev) {
const stats = yield statAsync(filePath);
const lastModified = new Date(stats.mtime).toUTCString();
res.setHeader('Last-Modified', lastModified);
res.setHeader('Cache-Control', 'public');
const ifModifiedSince = req.headers['if-modified-since'];
if (ifModifiedSince === lastModified) {
res.statusCode = 304;
res.end();
return;
}
}
res.setHeader('Content-Type', (_a = mime.getType(filePath), (_a !== null && _a !== void 0 ? _a : 'text/plain')));
const stream = fs.createReadStream(filePath);
stream.pipe(res);
stream.on('close', () => {
res.end();
});
}
else {
res.statusCode = 404;
res.end();
}
});
}
const Link = (_a) => {
var { to, onClick } = _a, otherProps = __rest(_a, ["to", "onClick"]);
const handler = React.useCallback((evt) => {
if (onClick) {
onClick(evt);
}
if (!evt.isDefaultPrevented()) {
evt.preventDefault();
history.push(to);
}
}, [to, onClick]);
return (React.createElement("a", Object.assign({ onClick: handler, href: to }, otherProps)));
};
const config$2 = getConfig();
function createServer(options) {
const app = http.createServer((req, res) => __awaiter(this, void 0, void 0, function* () {
var _a;
if ((_a = req.url) === null || _a === void 0 ? void 0 : _a.startsWith(config$2.publicPath)) {
sendStaticFile(req, res);
}
else {
const html = yield renderToString(req, res, req.url || '/', options);
res.end(html);
}
}));
app.listen(config$2.port, () => {
console.log(`Application running on http://localhost:${config$2.port}`);
});
}
function render(options) {
createServer(options);
}
exports.Link = Link;
exports.history = history;
exports.render = render;
exports.renderToString = renderToString;
exports.useLocation = useLocation;
exports.useParams = useParams;
;