## 入口 以webpack的entry配置作为入口,这里可分为单页面开发或多页面 单页面模式,webpack入口只有一个,匹配所有路由引入此入口输出 多页面模式,webpack有多个入口,entry配置名称映射路由名,引入对应匹配的入口输出 ## 服务端使用客户端代码 目前主流的ssr做法都是打两套js,但其实分析一些两套js的代码其中百分之99的代码都是重复的? 打两套包复杂度也高了,原来webpack中只用判断开发和生产模式现在多了客户端和服务端的判断 所以为何不能通过webpack赋予的能力,提供插件,通过注入差异的代码,让一套客户端的代码同时跑在服务器和客户端 ### 实现方式 开始的实现方式是通过提供插件修改webpack打出的chunk包,之后输出方式换成了umd形式更简单方便,扩展性更好 #### webpack插件 绑定chunkTemplate的钩子函数,注入代码到输出源码中 ```javascript // 钩子函数 compilation.chunkTemplate.hooks.render.tap('ChunksPluginRenderHack', (modules) => { ... }) // 实际注入代码后的输出 (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else { var a = factory(); for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i]; } })(window, function() { return (function() { if (!Promise._all) { ... } var __global_variable__ = typeof window === "undefined" ? global : window return (__global_variable__["webpackJsonp"] = __global_variable__["webpackJsonp"] || []).push( [ ["main"], { module: (function(module, exports, __webpack_require__) {}), ... }, [["entryModuleId", "webpack-runtime", ...dependenciesChunkId]] ] ) })()["default"]; }); ``` 用匿名函数包一层,隔离作用域,避免写入全局变量 判断module变量是否存在,node端不允许声明module的同名变量,umd方式后无需关心 通过webpack插件修改window为__global_variable__,避免正常的typeof window === 'undefined'判断失效 #### umd打包方式 ```javascript (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else { var a = factory(); for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i]; } })(window, function() { ... })()["default"]; }); ``` ## 组件渲染 挂载到真实dom节点上的代码不能出现在代码中,否则服务端执行时会报错,因此用户的webpack的入口执行代码应该仅仅是抛出一个react组件 服务端引用到这个组件调用react服务端渲染api渲染出html 而客户端也要找到这个入口组件使用react-dom提供的hydrate/render函数将组件挂在到真实dom上 这就需要我们提供客户端的挂载代码,这里我们提供并且通过push进webpack的entry配置中作为入口打包进来,在服务端不执行,客户端执行 ### 入口模块注册 提供的webpack插件会绑定moduleTemplates的钩子函数,查找包装入口模块 包装后的入口模块会调用全局函数__SSR_REGISTER_PAGE__,传入当前模块所属的chunkName和当前模块的输出 全局函数__SSR_REGISTER_PAGE__需要服务端和客户端都提供,这里主要是客户端需要,服务端仅仅是提供防止报错 优先判断__SSR_REGISTER_PAGE__是否存在,这里主要是为了兼容其他使用lissom/webpack打出不需要ssr的包能够正常使用,并且会给出warn提示 ```javascript const __SSR_REGISTER_PAGE__ = '!(typeof __SSR_REGISTER_PAGE__ !== "undefined" ? __SSR_REGISTER_PAGE__ : function(r, f) { console.warn(r+" chunk not find __SSR_REGISTER_PAGE__ function");f() })'; // 包装入口模块注册函数,供客户端查找调用 const source = new ConcatSource( `${__SSR_REGISTER_PAGE__}('${chunk.name}', function() {\n`, moduleSourcePostModule, '\nreturn { page: module.exports.default }', '});' ); // 服务端端提供 global.__SSR_REGISTER_PAGE__ = function (route, fn) { const { page } = fn() return page } // 客户端注册函数 window.__SSR_LOADED_PAGES__ = [routeName]; // 这里立即调用f()函数的原因很简单,代码需要被立即执行进入installModules中 window.__SSR_REGISTER_PAGE__ = function(r, f){ __SSR_LOADED_PAGES__.push([r, f()]) }; // 取出当前页面在服务端渲染的路由 const initialRoute = window.__SSR_LOADED_PAGES__.shift() ... const routers = {} const registerPage = (route, { page: Component }) => { routers[route] = Component if (isInitialRender && route === initialRoute) { const appContainer = document.getElementById('__ssr_root__') renderReactElement( , appContainer ) } } window.__SSR_LOADED_PAGES__.forEach(([r, f]) => { registerPage(r, f) }) delete window.__SSR_LOADED_PAGES__ window.__SSR_REGISTER_PAGE__ = registerPage ```