UNPKG

server-renderer

Version:

library of server side render for React

310 lines (291 loc) 11.7 kB
'use strict'; 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;