SinglePageApplication(SPA)模式被越来越多的站点采用,这种模式是指使用JavaScript直接在浏览器中渲染页面。所有的逻辑、数据获取、模板和路由都在客户端处理,势必面临首次有效绘画(FMP)耗时较长,不利于搜索引擎优化(SEO)的问题。“通用”是指一套代码可以同时运行在服务端和客户端环境中。利用这种灵活性,可以在服务端渲染初始内容并输出到页面,后续的工作可以交给客户端。大功告成,终于解决了SEO问题,提升了性能。“同构应用”就像一只小精灵,可以在服务端和客户端之间穿梭自如。然而,如果要控制“同构应用”,往往会面临一系列的问题。下面以一个例子来介绍一些细节。示例代码:https://github.com/xyyjk/reac...构建配置选择一个灵活的脚手架对于项目后续的自定义功能和配置是非常有好处的。Neutrino提供了一些常用的Webpack预置配置,这些预置包含了一些开发过程中常用的插件和配置,使初始化和构建项目的过程更加简单。让我们根据预设进行一些自定义配置,您可以随时运行node_modules/.bin/neutrino--inspect查看最终完整的Webpack配置。客户端配置这里是一些基于@neutrinojs/reactpresets开发的定义。neutrinorc.jsconstisDev=process.env.NODE_ENV!=='production';constisSSR=process.argv.includes('--ssr');module.exports={使用:[['@neutrinojs/react',{devServer:{端口:isSSR?3000:5000,host:'0.0.0.0',disableHostCheck:true,contentBase:`${__dirname}/src`,before(app){if(isSSR){require('./src/server')(app);}},},清单:true,html:isSSR?false:{},clean:{paths:['./node_modules/.cache']},}],({config})=>{if(isDev){return;}config.output.filename('assets/[name].[chunkhash].js').chunkFilename('assets/chunk.[chunkhash].js').end().optimization.minimize(false).end();},],};为了实现开发环境,可以选择SSR(server-siderendering)、CSR(ClientRendering)任意渲染模式,通过定义变量isDev、isSSR进行差分配置:devServer.before方法可以提供执行自定义中间件的功能在服务内的所有其他中间件之前。SSR方式增加了一个中间件,用于后期处理服务端组件内容渲染,同时很好的利用了devServer.hot的热更新功能。SSR方式使用动态定义的html模板(src/server/template.js),这里去掉底层的html-webpack-plugin。启用manifest插件,打包后生成资源映射文件,用于服务端渲染时导入到模板中。服务器端配置构建的配置项与服务器端运行略有不同。由于SSR模式的最终代码需要在node环境下运行,所以这里的配置需要做一些调整:target调整为node,编译成类Node环境时,libraryTarget可以调整为commonjs2,使用Node风格导出模块@babel/preset-env调整运行环境为node,编译结果为ES6代码,排除组件中对css/sass资源的引用,生产环境直接使用通过构建的映射文件读取资源的manifest插件在打包的时候通过webpack-node-externals排除node_modules依赖的模块可以让服务器构建更快,生成的bundle文件更小。webpack.server.config.jsconstNeutrino=require('neutrino/Neutrino');constnodeExternals=require('webpack-node-externals');constNormalPlugin=require('webpack/lib/NormalModuleReplacementPlugin');constbabelMerge=require('babel-merge');constconfig=require('./.neutrinorc');constneutrino=newNeutrino();neutrino.use(config);neutrino.config.target('node').entryPoints.delete('index').end().entry('server').add(`${__dirname}/src/server`).end().output.path(`${__dirname}/build`).filename('server.js').libraryTarget('commonjs2').end().externals([nodeExternals()]).plugins.delete('clean').delete('manifest').end().plugin('正常').use(NormalPlugin,[/\.css$/,'lodash/noop']).end().optimization.minimize(false).runtimeChunk(false).end().module.rule('compile').use('babel').tap(options=>babelMerge(options,{presets:[['@babel/preset-env',{targets:{node:true},}],],}));module.exports=neutrino.config.toConfig();环境差异由于运行环境和平台API的差异,在不同的环境运行时,我们的代码不会完全一样。Webpack全局对象中定义了process.browser,在开发环境中可以判断是客户端还是服务端。在自定义中间件开发环境的SSR模式下,如果我们在组件中引入图片或者样式资源,不通过webpack-loader编译,是无法直接在Node环境中运行的。在Node环境下,可以通过ignore-styles忽略这些资源。另外,为了在Node环境下运行ES6模块组件,需要引入@babel/register做一些转换:src/server/register.jsrequire('ignore-styles');require('@babel/register')({presets:[['@babel/preset-env',{targets:{node:true},}],'@babel/preset-react',],plugins:['@babel/plugin-proposal-类-属性',],});如果webpack中配置了resolve.alias,相应的需要添加babel-plugin-module-resolver插件进行解析。清除模块缓存。由于require()的导入方法,模块将被缓存。为了让组件中的修改实时生效,通过decache模块从require()缓存中删除模块,重新引用:src/server/dev.jsrequire('./register');constdecache=require('decache');constroutes=require('./routes');让render=require('./render');consthandler=async(req,res,next)=>{decache('./render');render=require('./render');res.send(awaitrender({req,res}));next();};module.exports=(app)=>{app.get(routes,handler);};服务器端渲染组件通过ReactDOMServer.renderToString()方法在服务器端渲染为初始HTML字符串。获取数据往往需要从query和cookie中取出一些内容作为接口参数。Node环境中没有window、document等浏览器对象。你可以使用Express的req对象来获取一些信息:href:${req.protocol}://${req.headers.host}${req.url}cookie:req.headers.cookieuserAgent:req.headers['用户代理']src/server/render.jsconstReact=require('react');const{renderToString}=require('react-dom/server');...module.exports=async({req,res})=>{constlocals={data:awaitfetchData({req,res}),href:`${req.protocol}://${req.headers.host}${req.url}`,url:req.url,};constmarkup=renderToString(
