前言

​ 在博客开发的过程中,有这样一个需求想解决,就是在SSR开发环境中,服务端的代码是是直接通过webpack打包成文件(因为里面包含同构的代码,就是服务端与客户端共享前端的组件代码),写到磁盘里,然后在启动打包好的入口文件来启动服务。但是我不想在开发环境把文件打包到磁盘中,想直接打包在内存中,这样不仅能优化速度,还不会因开发环境产生多余文件。还有就是webpack对require的处理,会导致路径映射的问题,包括对require变量的问题。所以我就想只有组件相关的代码进行webpack编译,别的无关的服务端代码不进行webpack编译处理。

但是这中间有个问题一直悬而不决,就是如何引入内存中的文件。包括在引入这个文件后,如何把关联的文件一起引入,如通过require(module)引入的模块,于是我想到以前在给vue做ssr的时候用到的vue-server-renderer这个库,这个是没有直接打出文件,而是把文件打入了内存中。但是他却能获取到文件,并执行文件获取到结果。于是就开启了这次的研究之旅。

实现

先讲下项目这块的实现流程,然后在讲下vue-server-renderer这个包是如何解决这个问题的,以此在react中的实现。

|-- webpack
| |-- webpack.client.js // entry => clilent-main.js
| |-- webpack.server.js // entry => server-main.js
|-- client // 客户端代码
| |-- app.js
| |-- client-main.js // 客户端打包入口
| |-- server-main.js // server端打包代码入口
|-- server // server端代码
| |-- ssr.js // ssr启动入口
  1. client-main.js, 客户端打包一份代码,就是正常的打包, 打包出对应的文件。

    import React, { useEffect, useState } from 'react'
    import ReactDom from 'react-dom'
    import App from './app' loadableReady(() => {
    ReactDom.hydrate(
    <Provider store={store}>
    <App />
    </Provider>,
    document.getElementById('app')
    )
    })
  2. server-main.js,因为是SSR,所以在服务端也需要打包一份对应的js文件,用于ssr渲染。我这里是打算在这块直接处理完组件相关的数据,返回html,到时候服务端直接引入这个文件,获取html返回给前端就行。这是我的项目的处理,vue官方demo会有点区别,他是直接返回的app实例(new Vue(...), 然后在vue-server-renderer库中解析这个实例,最后同样也是返回解析好的html字符串。这里会有点区别,原理还是一样。

    // 返回一个函数,这样可以传入一些参数,用来传入服务端的一些数据
    import { renderToString } from 'react-dom/server'
    export default async (context: IContext, options: RendererOptions = {}) => {
    // 获取组件数据
    ... // 获取当前url对应的组件dom信息
    const appHtml = renderToString(
    extractor.collectChunks(
    <Provider store={store}>
    <StaticRouter location={context.url} context={context as any}>
    <HelmetProvider context={helmetContext}>
    <App />
    </HelmetProvider>
    </StaticRouter>
    </Provider>
    )
    ) // 渲染模板
    const html = renderToString(
    <HTML>{appHtml}</HTML>
    )
    context.store = store
    return html
    }

3. `ssr.js`, 因为这些文件我都是打在内存中的。所以我需要解析内存中的文件,来获取`server-main.js`中的函数,执行他,返回html给前端。 ```typescript
// start方法是执行webpack的node端代码,用于把编译的文件打入内存中。
import { start } from '@root/scripts/setup' // 执行他,createBundleRenderer方法就是用来解析在server端打包的代码
start(app, ({ loadableStats, serverManifest, inputFileSystem }) => {
renderer = createBundleRenderer({
loadableStats,
serverManifest,
inputFileSystem
})
}) // 执行server-main.js中的函数并获取html
const html = await renderer.renderToString(context)
ctx.body = html

客户端的好说,通过创建html模板,然后把当前路由对应的资源(js, css,..)引入,访问的时候,浏览器直接拉取资源就行(这块是通过@loadable/webpack-plugin@loadable/server@loadable/component来进行资源的加载与获取,此处不做过多介绍,此文重点不在这个)。

这块的重点就是如何在内存中解析server-main.js这个被打包出来的需要在服务端引用的代码。

我们来看vue ssr的官方代码: vue-hackernews-2.0

const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge(base, {
target: 'node',
devtool: '#source-map',
entry: './src/server-main.js',
output: {
filename: 'server-bundle.js',
libraryTarget: 'commonjs2'
},
plugins: [
new VueSSRServerPlugin()
]
})

上面用到了一个vue-server-renderer/server-plugin, 这个插件的主要功能是干嘛呢,其实就是对webpack中的资源做了下处理,把其中的js资源全部打在了一个json文件中。

源码如下:

// webpack上自定义了一个vue-server-plugin插件
compiler.hooks.emit.tapAsync('vue-server-plugin', (compilation, cb) => {
// 获取所有资源
var stats = compilation.getStats().toJson();,
var entryName = Object.keys(stats.entrypoints)[0];
var entryInfo = stats.entrypoints[entryName]; // 不存在入口文件
if (!entryInfo) {
return cb()
}
var entryAssets = entryInfo.assets.filter(isJS); // 入口具有多个js文件,只需一个就行: entry: './src/entry-server.js'
if (entryAssets.length > 1) {
throw new Error(
"Server-side bundle should have one single entry file. " +
"Avoid using CommonsChunkPlugin in the server config."
)
} var entry = entryAssets[0];
if (!entry || typeof entry !== 'string') {
throw new Error(
("Entry \"" + entryName + "\" not found. Did you specify the correct entry option?")
)
} var bundle = {
entry: entry,
files: {},
maps: {}
};
// 遍历所有资源
stats.assets.forEach(function (asset) {
// 是js资源,就存入bundle.files字段中。
if (isJS(asset.name)) {
bundle.files[asset.name] = compilation.assets[asset.name].source();
} else if (asset.name.match(/\.js\.map$/)) { // sourceMap文件,存入maps字段中,用来追踪错误
bundle.maps[asset.name.replace(/\.map$/, '')] = JSON.parse(compilation.assets[asset.name].source());
}
// 删除资源,因为js跟js.map已经存到bundle中了,需要的资源已经存起来了,别的没必要打包出来了。
delete compilation.assets[asset.name];
}); var json = JSON.stringify(bundle, null, 2);
var filename = this$1.options.filename; // => vue-ssr-server-bundle.json // 把bundle存入assets中,那样assets中就只有vue-ssr-server-bundle.json这个json文件了,
/*
vue-ssr-server-bundle.json
{
entry: 'server-bundle.js',
files: [
'server-bundle.js': '...',
'1.server-bundle.js': '...',
],
maps: [
'server-bundle.js.map': '...',
'1.server-bundle.js.map': '...',
]
}
*/
compilation.assets[filename] = {
source: function () { return json; },
size: function () { return json.length; }
};
cb();
});

这个插件的处理也及其简单,就是拦截了资源,对其重新做了下处理。生成一个json文件,到时候方便直接进行解析处理。

然后我们来看node服务的入口文件,来看如何获取html,并进行解析的

const { createBundleRenderer } = require('vue-server-renderer')
// bundle: 读取vue-ssr-server-bundle.json中的数据,
/*
bundle => vue-ssr-server-bundle.json
{
entry: 'server-bundle.js',
files: [
'server-bundle.js': '...',
'1.server-bundle.js': '...',
],
maps: [
'server-bundle.js.map': '...',
'1.server-bundle.js.map': '...',
]
}
*/
renderer = createBundleRenderer(bundle, {
template: fs.readFileSync(templatePath, 'utf-8'), // html模板
// client端json文件,也存在于内存中,也是对webpack资源的拦截处理,这里不做多介绍,原理差不多。读取对应的资源放入html模板中,在client端进行二次渲染,绑定vue事件等等
clientManifest: readFile(devMiddleware.fileSystem, 'vue-ssr-client-manifest.json'),
runInNewContext: false // 在node沙盒中共用global对象,不创建新的
}))
const context = {
title: 'Vue HN 2.0', // default title
url: req.url
}
renderer.renderToString(context, (err, html) => {
if (err) {
return handleError(err)
}
res.send(html)
})

通过查看上面server端项目启动的入口文件,里面用createBundleRenderer中的renderToString来直接返回html,所以来到vue-server-renderer这个库来看看这个里面到底做了什么

function createRenderer(ref) {
return {
renderToString: (app, context, cb) => {
// 解析app: app => new Vue(...),就是vue实例对象
// 这块就是对vue组件的编译解析,最后获取对应的html string
// 重点不在这,此处也不做过多介绍
const htmlString = new RenderContext({app, ...})
return cb(null, htmlString)
}
}
}
function createRenderer$1(options) {
return createRenderer({...options, ...rest})
}
function createBundleRendererCreator(createRenderer) {
return function createBundleRenderer(bundle, rendererOptions) {
entry = bundle.entry;
// 关联的js资源内容
files = bundle.files;
// sourceMap内容
// createSourceMapConsumers方法作用便是通过require('source-map')模块来追踪错误文件。因为我们都进行了资源拦截,所以这块也需要自己实现对错误的正确路径映射。
maps = createSourceMapConsumers(bundle.maps); // 调用createRenderer方法获取renderer对象
var renderer = createRenderer(rendererOptions); // 这块就是处理内存文件中的代码了,
// {files: ['entry.js': 'module.exports = a']}, 就是我读取entry.js文件中的内容,他是字符串, 然后node如何处理的,处理完之后得到结果。
// 下面这个方法进行详细说明
var run = createBundleRunner(
entry,
files,
basedir,
rendererOptions.runInNewContext
); return {
renderToString: (context, cb) => {
// 执行run方法,就能获取我在server-main.js入口文件里面 返回的new Vue实例
run(context).then(app => {
renderer.renderToString(app, context, function (err, res) {
// 打印错误映射的正确文件路径
rewriteErrorTrace(err, maps);
// res: 解析好的html字符串
cb(err, res);
});
})
}
}
}
}
var createBundleRenderer = createBundleRendererCreator(createRenderer$1);
exports.createBundleRenderer = createBundleRenderer;
  1. 上面逻辑也比较清晰明了,通过createBundleRunner方法来解析入口文件的字符串代码,vue server-main.js入口文件返回是一个Promise函数,Promise返回的是new Vue(),所以解析出来的结果就new Vue实例。
  2. 通过RenderContext等实例解析返回的new Vue实例,获取到对应的html字符串。
  3. 通过source-map模块对错误进行正确的文件路径映射。

这样就实现了在内存中执行文件中的代码,返回html,达到ssr的效果。这次文章的重点是如何执行那段入口文件的 字符串 代码。

我们来到createBundleRunner方法,来看看里面到底是如何实现的。


function createBundleRunner (entry, files, basedir, runInNewContext) {
var evaluate = compileModule(files, basedir, runInNewContext);
if (runInNewContext !== false && runInNewContext !== 'once') {
// 这块runInNewContext不传false 跟 once这两个选项的话,每次都会生成一个新的上下文环境,我们共用一个上下文global就行。所以这块就不考虑
} else {
var runner;
var initialContext;
return function (userContext) {
// void 0 === undefined, 因为undefined可被重新定义,void没法重新定义,所以用void 0 肯定是undefined
if ( userContext === void 0 ) userContext = {}; return new Promise(function (resolve) {
if (!runner) {
// runInNewContext: false, 所以这里上下文就是指的global
var sandbox = runInNewContext === 'once'
? createSandbox()
: global;
// 通过调用evaluate方法返回入口文件的函数。代码实现: evaluate = compileModule(files, basedir, runInNewContext)
// 去到compileModule方法看里面是如何实现的
/*
vue官方demo的server-main.js文件,返回的时一个Promise函数,所以runner就是这个函数。
export default context => {
return new Promise((resolve) => {
const { app } = createApp()
resolve(app)
})
}
*/
// 传入入口文件名,返回入口函数。
runner = evaluate(entry, sandbox);
}
// 执行promise返回 app,至此app就得到了。
resolve(runner(userContext));
});
}
}
} // 这个方法返回了evaluateModule方法,也就是上面evaluate方法
// evaluate = function evaluateModule(filename, sandbox, evaluatedFiles) {}
function compileModule (files, basedir, runInNewContext) {
var compiledScripts = {}; // filename: 依赖的文件名,例如 server.bundle.js 或 server.bundle.js依赖的 1.server.bundle.js 文件
// 在通过vue-ssr-server-bundle.json中的files字段获取这个文件名对应的文件内容 类似:"module.exports = 10"字符串
// 通过node的module模块来包裹这段代码,代码其实很简单粗暴,封装成了一个函数,传入我们熟知的commonjs规范中的require、exports等等变量
/*
Module.wrapper = [
'(function (exports, require, module, __filename, __dirname, process, global) { ',
'\n});'
];
Module.wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
}; 结果:
function (exports, require, module, __filename, __dirname, process, global) {
module.exports = 10
}
*/
// 通过vm模块创建沙盒环境,来执行这段js代码。
function getCompiledScript (filename) {
if (compiledScripts[filename]) {
return compiledScripts[filename]
}
var code = files[filename];
var wrapper = require('module').wrap(code);
var script = new require('vm').Script(wrapper, {
filename: filename,
displayErrors: true
});
compiledScripts[filename] = script;
return script
} function evaluateModule (filename, sandbox, evaluatedFiles) {
if ( evaluatedFiles === void 0 ) evaluatedFiles = {}; if (evaluatedFiles[filename]) {
return evaluatedFiles[filename]
} // 获取这个执行这段代码的沙盒环境
var script = getCompiledScript(filename);
// 沙盒环境使用的上下文 runInThisContext => global
var compiledWrapper = runInNewContext === false
? script.runInThisContext()
: script.runInNewContext(sandbox);
var m = { exports: {}};
var r = function (file) {
file = path$1.posix.join('.', file);
// 当前js依赖的打包文件,存在,继续创建沙盒环境执行
if (files[file]) {
return evaluateModule(file, sandbox, evaluatedFiles)
} else {
return require(file)
}
};
// 执行函数代码。注意webpack要打包成commonjs规范的,不然这里就对不上了。
compiledWrapper.call(m.exports, m.exports, r, m);
// 获取返回值
var res = Object.prototype.hasOwnProperty.call(m.exports, 'default')
? m.exports.default
: m.exports;
evaluatedFiles[filename] = res;
// 返回结果
return res
}
return evaluateModule
}

createBundleRunner函数里的实现其实也不多。就是创建一个沙盒环境来执行获取到的代码

整个逻辑核心思路如下

  1. 通过拦截webpack assets 生成一个json文件,包含所有js文件数据
  2. 通过入口文件到生成好的json文件里面取出来那段字符串代码。
  3. 通过require('module').wrap把字符串代码转换成函数形式的字符串代码,commonjs规范
  4. 通过require('vm')创建沙盒环境来执行这段代码,返回结果。
  5. 如果入口文件有依赖别的文件,再次执行 2 - 4步骤,把入口文件换成依赖的文件就好,例如,路由一般都是懒加载的,所以在访问指定路由时,webpack打包出来也会获取这个对应的路由文件,依赖到入口文件里面。
  6. 通过沙盒环境执行获取到的返回结果,在vue-hackernews-2.0项目中是 new Vue实例对象。
  7. 解析这个vue实例,获取到对应的html字符串,放入html模板中,最后返回给前端。

这样就实现了读取内存文件,得到对应的html数据。主要就是通过 vm模块跟module模块来执行这些代码的。其实这块的整个代码也还是比较简单的。并没有什么复杂的逻辑。

因为项目是基于reactwebpack5的,所以在代码的处理上会有些不同,但是实现方案基本还是一致的。

其实说到执行代码,js里面还有一个方法可以执行代码,就是eval方法。但是eval方法在require的时候都是在本地模块中进行查找,存在于内存中的文件我发现没法去进行require查找。所以还是用的vm模块来执行的代码,毕竟可以重写require方法

项目完整代码:GitHub 仓库

博客原文地址

我自己新创建了一个相互学习的群,无论你是准备入坑的小白,还是半路入行的同学,希望我们能一起分享与交流。

QQ群:810018802, 点击加入

QQ群 公众号
前端打杂群
冬瓜书屋

解读vue-server-renderer源码并在react中的实现的更多相关文章

  1. Go Revel - server.go 源码分析

    之前介绍了 Go Revel - main函数分析 http://www.cnblogs.com/hangxin1940/p/3263775.html 最后会调用 `revel.Run(*port)` ...

  2. 从vue.js的源码分析,input和textarea上的v-model指令到底做了什么

    v-model是 vue.js 中用于在表单表单元素上创建双向数据绑定,它的本质只是一个语法糖,在单向数据绑定的基础上,增加了监听用户输入事件并更新数据的功能:对,它本质上只是一个语法糖,但到底是一个 ...

  3. 一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构

    1.前言 本文内容讲解的内容:一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构. 项目地址:https://github.com/biaochenxuying/vu ...

  4. 安卓图表引擎AChartEngine(四) - 源码示例 嵌入Acitivity中的折线图

    前面几篇博客中都是调用ChartFactory.get***Intent()方法,本节讲的内容调用ChartFactory.get***View()方法,这个方法调用的结果可以嵌入到任何一个Activ ...

  5. Spring5源码解析-Spring框架中的单例和原型bean

    Spring5源码解析-Spring框架中的单例和原型bean 最近一直有问我单例和原型bean的一些原理性问题,这里就开一篇来说说的 通过Spring中的依赖注入极大方便了我们的开发.在xml通过& ...

  6. angular源码分析:angular中脏活累活的承担者之$interpolate

    一.首先抛出两个问题 问题一:在angular中我们绑定数据最基本的方式是用两个大括号将$scope的变量包裹起来,那么如果想将大括号换成其他什么符号,比如换成[{与}],可不可以呢,如果可以在哪里配 ...

  7. angular源码分析:angular中入境检察官$sce

    一.ng-bing-html指令问题 需求:我需要将一个变量$scope.x = '<a href="http://www.cnblogs.com/web2-developer/&qu ...

  8. angular源码分析:angular中各种常用函数,比较省代码的各种小技巧

    angular的工具函数 在angular的API文档中,在最前面就是讲的就是angular的工具函数,下面列出来 angular.bind //用户将函数和对象绑定在一起,返回一个新的函数 angu ...

  9. angular源码分析:angular中的依赖注入式如何实现的

    一.准备 angular的源码一份,我这里使用的是v1.4.7.源码的获取,请参考我另一篇博文:angular源码分析:angular源代码的获取与编译环境安装 二.什么是依赖注入 据我所知,依赖注入 ...

随机推荐

  1. c语言链表从本地文件中读取和写入数据

    1 typedef struct Data{ 2 40 char *name; 3 41 char *IDCARD; 4 42 char *job_id; 5 43 char *length; 6 4 ...

  2. Linux系统(Centos7)最新版本Docker简易(yum)安装步骤

    Docker从1.13版本之后采用时间线的方式作为版本号,分为社区版CE和企业版EE. 社区版是免费提供给个人开发者和小型团体使用的,企业版会提供额外的收费服务,比如经过官方测试认证过的基础设施.容器 ...

  3. 060_Cookie/Session

    目录 会话 有状态会话 保存会话的两种技术 cookie session 常见场景 Cookie cookie细节 删除cookie Session 什么是session session使用场景 se ...

  4. x64dbg 条件断点相关文档

    输入 字符格式 条件断点 Input When using x64dbg you can often use various things as input. Commands Commands ha ...

  5. [源码解析] 并行分布式框架 Celery 之 worker 启动 (1)

    [源码解析] 并行分布式框架 Celery 之 worker 启动 (1) 目录 [源码解析] 并行分布式框架 Celery 之 worker 启动 (1) 0x00 摘要 0x01 Celery的架 ...

  6. java例题_03 水仙花数

    1 /*3 [程序 3 水仙花数] 2 题目:打印出所有的"水仙花数",所谓"水仙花数"是指一个三位数,其各位数字立方和等于该数本身. 3 例如:153 是一个 ...

  7. Android Studio 之 制作 Nine-Patch 图片(.9图片)

    •引言 9.png 可以保证图片在合适的位置进行局部拉伸,避免了图片全局缩放造成的图片变形问题. 但是由于Android Studio对于.9图片的检查更加严格,所以不符合AS要求的.9图片会带来很多 ...

  8. [Fundamental of Power Electronics]-PART II-9. 控制器设计-9.5 控制器的设计

    9.5 控制器设计 现在让我们来考虑如何设计控制器系统,来满足有关抑制扰动,瞬态响应以及稳定性的规范或者说设计目标.典型的直流控制器设计可以用以下规范定义: 1.负载电流变化对输出电压调节的影响.当负 ...

  9. 通过Dapr实现一个简单的基于.net的微服务电商系统(五)——一步一步教你如何撸Dapr之状态管理

    状态管理和上一章的订阅发布都算是Dapr相较于其他服务网格框架来讲提供的比较特异性的内容,今天我们来讲讲状态管理. 目录:一.通过Dapr实现一个简单的基于.net的微服务电商系统 二.通过Dapr实 ...

  10. 自动化kolla-ansible部署ubuntu20.04+openstack-victoria之vmware设置-02

    自动化kolla-ansible部署ubuntu20.04+openstack-victoria之vmware设置-02 欢迎加QQ群:1026880196  进行讨论 1. vmwae版本 2. 网 ...