当前位置: 首页 > news >正文

手动创建 vue2 ssr 开发环境

本文和个人博客同步发表
更多优质文章查看个人博客

前言

手动搭建 vue ssr 一直是一些前端开发者的噩梦,因为其中牵扯到很多依赖包之间的配置以及webpack在node中的使用。就拿webpack配置来说,很多前端开发者还是喜欢用webpack-cli脚手架搭建项目。导致这样的原因之一无外乎学习成本高,软件复杂等。这也是有些前端开发者直接拥抱nuxt.js的部分原因。这篇博客使用vue2,以步骤为主,来展示如何创建完整ssr开发环境。

主要参考文章

  1. vue2 ssr 中文官网
  2. webpack 中文官网
  3. webpack-dev-middleware
  4. webpack-hot-middleware
  5. webpack tapable
  6. memory-fs
  7. express
  8. vue2 ssr 官方参考案例

构建ssr所需依赖包

{"devDependencies": {"chokidar": "^3.5.3","css-loader": "^6.7.3","memory-fs": "^0.5.0","vue-loader": "^15.9.8","vue-style-loader": "^4.1.3","webpack": "^5.54.0","webpack-dev-middleware": "^5.2.1","webpack-hot-middleware": "^2.25.1","webpack-node-externals": "^3.0.0"},"scripts": {"dev": "node ./server/index.cjs"},"dependencies": {"express": "^4.18.2","vue": "^2.6.14","vue-router": "^3.5.2","vue-server-renderer": "^2.6.14","vue-template-compiler": "^2.6.14","vuex": "^3.6.2","vuex-router-sync": "^5.0.0"}
}

目录结构

需要创建如下图的目录结构,方能进行后面的代码编写
请添加图片描述

ssr是如何生成的?

了解完目录架构之后,首先需要知道ssr是如何生成的是至关重要的,只有这样我们才了解后续通过什么样的操作来构建ssr。首先看一张官方给出的构建图。
请添加图片描述

从图中可以看出,想要实现ssr,必选通过webpack构建生成的服务端bundle文件和客户端bundle文件,服务端bundle在bundle renderer的作用下生成html字符串,并发送给浏览器端并且和客户端bundle一起作用下激活,最终实现ssr。代码中创建html字符串和发送到浏览器的实现如下所示

let devServerPromise = devServer((serverBundle, options) => {// 服务端生成的bundele和客户端生成的clientManifest结合,并返回rendererrenderer = createBundleRenderer(serverBundle, Object.assign(options, {runInNewContext: false,}))
});ROUTER.get('*', (req, res) => {const context = {url: req.url}devServerPromise.then((random) => {// 将 Vue 实例渲染为字符串,发送给客户端renderer.renderToString(context).then(html => {res.send(html)}).catch(err => {console.log('err',req.url,err)})})
})

其实在构建图中还少一个关键点,如下所示
请添加图片描述

webpack在编译客户端时会生成客户端构建清单(clientManifest),清单里面的内容其实是当html字符串在浏览器中解析时要获取的资源内容(也可简单理解为client bundle),当解析html时根据内容去请求client bundle。

好!到目前为止,实现vue ssr思路已经很清晰了。在这里简单梳理一下。

需要
需要
依赖
依赖
生成
生成
生成
生成html字符串
webapck
clientBundle
renderToString函数
serverBundle
clientManifest

根据上面的图可知,想要实现ssr,就是需要serverBundle、clientBundle、clientManifest文件。而这三种文件又是通过webpack生成的。所以现在的要面临的问题就是配置webpack,生成这三种文件,然后通过renderToString函数实现ssr。话不多说开始配置。

配置 webpack

在配置webpack之前,需要先创建要打包的代码、app.js、entry-client.js和entry-server.js。创建这些内容的作用是为后续webpack打包做准备。

1. 创建UAC(Universal Application Code)相关文件

请添加图片描述

在ssr-demo文件夹下的client文件夹中,分别创建router、store和views文件夹和相关内容。

1.1 router文件夹和相关内容


// 路径 client/router/index.js
import Vue from 'vue'
import Router from 'vue-router'Vue.use(Router);export function createRouter(){return new Router({mode:'history',routes:[{path:'/',component: () => import('../views/index.vue')},]})
}

1.2 store文件夹和相关内容


// 路径 client/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'Vue.use(Vuex);export function createStore(){return new Vuex.Store({state:{},actions:{},mutations:{}})
}

1.3 view文件夹和相关内容


<!-- 路径 client/views/index.vue -->
<template><div>kinghiee ssr test</div>
</template><script>
export default {}
</script><style></style>

2. 创建app.js文件

请添加图片描述


// 路径 client/app.js
import Vue from 'vue';
import App from './App.vue';
import { createRouter } from './router/index';
import { createStore } from './store';
import { sync } from 'vuex-router-sync';// 简单工厂模式创建vue实例
export function createApp() {const router = createRouter();const store = createStore();sync(store, router);const app =  new Vue({router,store,render: h => h(App),})return {app,router,store}
};

app.js文件的作用: 使用简单工厂模式,创建vue实例,为每个请求创建新的应用程序实例,避免状态单例。更加详细解释请查看官网;

3. 创建entry-client.js文件

请添加图片描述


// 路径 client/entry-client.js
import { createApp } from './app';
import Vue from 'vue';const { app, router, store } = createApp();Vue.mixin({/*** 路由更新触发组件内异步获取数据方法* @param {*} to* @param {*} from* @param {*} next*/beforeRouteUpdate(to, from, next) {const { asyncData } = this.$optionsif (asyncData) {asyncData({store: this.$store,route: to}).then(next).catch(next)} else {next()}}
});if (window.__INITIAL_STATE__) {store.replaceState(window.__INITIAL_STATE__);
}router.onReady(() => {router.beforeResolve((to, from, next) => {const matched = router.getMatchedComponents(to);const prevMatched = router.getMatchedComponents(from);let diffed = false;const activated = matched.filter((c, i) => {return diffed || (diffed = (prevMatched[i] !== c))})if (!activated.length) {return next();}Promise.all(activated.map(c => {if (c.asyncData) {return c.asyncData({ store, route: to })}})).then(() => {next();}).catch(next)})app.$mount('#app')
})

4. 创建entry-server.js文件

请添加图片描述

// 路径 client/entry-server.js
import { createApp } from './app'export default context => {return new Promise((resolve, reject) => {const { app, router, store } = createApp()router.push(context.url)router.onReady(() => {const matchedComponents = router.getMatchedComponents()if (!matchedComponents.length) {return reject({ code: 404 })}// 对所有匹配的路由组件调用 `asyncData()`Promise.all(matchedComponents.map(Component => {if (Component.asyncData) {return Component.asyncData({store,route: router.currentRoute})}})).then(() => {// 在所有预取钩子(preFetch hook) resolve 后,// 我们的 store 现在已经填充入渲染应用程序所需的状态。// 当我们将状态附加到上下文,// 并且 `template` 选项用于 renderer 时,// 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。context.state = store.stateresolve(app)}).catch(reject)}, reject)})
}

注:entry-client.js和entry-server.js文件以及文件中为什么这么写在官网中都能找的到,这里只说明搭建步骤 官网

相关代码创建完毕之后,client文件夹内容大致如下

请添加图片描述

到此为止,webpack打包前期的准备工作已经结束。接下来开始配置webpack打包

5. webpack配置

请添加图片描述
在ssr webpack打包配置中分为客户端和服务端配置。客户端打包配置主要生成后面用到的客户端bundle和clientManifest,而服务端打包配置主要生成后面用到的服务端bundle,这三种文件正是ssr所需的关键文件。

5.1 客户端打包配置

// 路径 build/webpack.client.dev.js
const { resolve: RESOLVE } = require('path');
const WEBPACK = require('webpack');
const { VueLoaderPlugin: VUELOADERPLUGIN } = require('vue-loader');
const VUESSRCLIENTPLUGIN = require('vue-server-renderer/client-plugin')module.exports = {mode: 'development',entry: { app: RESOLVE(__dirname, '../client/entry-client.js') },output: {path: RESOLVE(__dirname, '../dist'),filename: 'src/[name].[contenthash:6].js',publicPath: '/dist/'},module: {rules: [{test: /\.vue$/,loader: 'vue-loader'},{test: /\.css$/i,use: ["vue-style-loader", "css-loader"],},]},resolve: {extensions: ['.js', '.ts', '.vue', '.json'],alias: {'@client':RESOLVE(__dirname,'../client')}},plugins: [new VUELOADERPLUGIN(),new VUESSRCLIENTPLUGIN({filename: 'src/vue-ssr-client-manifest.json'})]
}

5.2 服务端打包配置


// 路径 build/webpack.server.dev.js
const { resolve: RESOLVE } = require('path');
const { VueLoaderPlugin: VUELOADERPLUGIN } = require('vue-loader');
const VUESERVERPlUGINSSR = require('vue-server-renderer/server-plugin')
const NODEEETERNALS = require('webpack-node-externals');module.exports = {target: 'node',devtool: 'eval-cheap-source-map',entry: RESOLVE(__dirname, '../client/entry-server.js'),output: {path: RESOLVE(__dirname, '../dist'),filename: 'server-bundle.js',libraryTarget: 'commonjs2'},module: {rules: [{test: /\.vue$/,loader: 'vue-loader'},{test: /\.css$/i,use: ["vue-style-loader", "css-loader"],},]},externalsPresets: { node: true }, // in order to ignore built-in modules like path, fs, etc.externals: [NODEEETERNALS()], // in order to ignore all modules in node_modules folderplugins: [new VUELOADERPLUGIN(),new VUESERVERPlUGINSSR({filename: 'src/vue-ssr-server-bundle.json'})]
}

webpack 客户端和服务端都已配置好了,那如何生成相应的三种文件呐?其实生成这三种文件需要在webpack编译阶段,而对于配置开发环境来说,一般还用到热更新和webpack dev中间件,所以webpack编译和热更新常在一起出现。目前为止该准备的都已经到位了,现在就可以开始webpack编译和配置热更新操作了。

webpack编译和配置热更新

在server文件夹内创建如下文件夹和文件
请添加图片描述
请添加图片描述

1. webpack编译

本博客给出的代码示例和官方的示例组织上有不同的地方,但功能上一样。本博客按照功能的不同对代码进行了合理的拆分和封装,而不是把全部功能写到一个函数下面。这样做的目的是关注点单一、功能单一、便于开发和维护。

1.1 编译客户端

// 路径 dev/clientCompile.cjs
let webpack = require('webpack');
let path = require('path');module.exports = function clientCompile(clientConfig, clientManifestCb) {// 向客户端 webpack 修改配置clientConfig.entry.app = [ 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=2000&reload=true', clientConfig.entry.app ];clientConfig.output.filename = '[name].[contenthash:6].js';clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin(),new webpack.NoEmitOnErrorsPlugin(),); // 编译客户端 webpack 配置let clientCompiler = webpack(clientConfig); // 获取 compiler 实例let devMiddleware = require('webpack-dev-middleware')(clientCompiler, {publicPath: clientConfig.output.publicPath,serverSideRender: true,stats: {//可选colors: true,modules: true,},});// done是AsyncSeriesHook类型钩子clientCompiler.hooks.done.tap('done', stats => {stats = stats.toJson({stats:'errors-warnings'});// 如果客户端编译完毕,有错误或者警告会打印到控制台stats.errors.forEach(err => console.error(err));stats.warnings.forEach(err => console.warn(err));// 有错误后续不生成 manifest 文件if (stats.errors.length) return;console.log('\n客户端更新...\n');let manifestContent = devMiddleware.context.outputFileSystem.readFileSync(path.resolve(clientConfig.output.path, 'src/vue-ssr-client-manifest.json'),'utf-8');clientManifestCb(JSON.parse(manifestContent));});let hotMiddleware = require('webpack-hot-middleware')(clientCompiler); return {devMiddleware,hotMiddleware}
}

如上代码所示,把编译客户端的代码写到一个文件内,并导处客户端编译函数,在其他地方用。

1.2 编译服务端

// 路径 dev/serverCompile.cjs
let webpack = require('webpack');
let path = require('path');
const MFS = require('memory-fs');module.exports = function serverCompile(serverConfig,serverBundleCb
) {let serverCompiler = webpack(serverConfig); let mfs = new MFS();serverCompiler.outputFileSystem = mfs; // 把 webpack 默认的普通文件系统更换为内存文件系统serverCompiler.watch({ ignored: /node_modules/, }, (err, stats) => {if (err) throw err;stats = stats.toJson();// 有错误后续不执行if (stats.errors.length) return;console.log('\n服务端更新...\n');// 获取服务端bundle文件路径let bundlePath = path.resolve(serverConfig.output.path,'src/vue-ssr-server-bundle.json');serverBundleCb(JSON.parse(mfs.readFileSync(bundlePath, 'utf-8')))});
}

写完客户端和服务端编译后,需要把函数导出来在后面的地方使用。代码如下

2. webpack编译和热更新配置

dev.cjs文件如下

// 路由 router/dev.cjs
const SERVER = require('express');
const ROUTER = SERVER.Router();
const FS = require('fs');
const PATH = require('path');
let clientConfig = require('../../build/webpack.client.dev');
let serverConfig = require('../../build/webpack.server.dev'); 
let templatePath =  PATH.resolve(__dirname, '../server.template.html');
let { createBundleRenderer } = require('vue-server-renderer')
let serverCompile = require('../dev/serverCompile.cjs');
let clientCompile = require('../dev/clientCompile.cjs');
let tempWatch = require('../dev/tempWatch.cjs');let renderer;const devServer = (cb) => {let clientManifest, serverBundle, readyResolve, templateContent;templateContent = FS.readFileSync(templatePath, 'utf-8');let readyPromise = new Promise(resolve => readyResolve = resolve );// 更新客户端和服务端内容let updateClientAndServer = () => {// 只有构建清单文件都存在时,执行更新操作if(clientManifest && serverBundle) {readyResolve(); // 把promise resolve掉cb(serverBundle, {template: templateContent,clientManifest})}};// 监听模板文件tempWatch(templatePath, () => {updateClientAndServer();});// 客户端 编译let { devMiddleware, hotMiddleware} = clientCompile(clientConfig, (clientManifestContent) => {clientManifest = clientManifestContent;updateClientAndServer();})ROUTER.use(devMiddleware);ROUTER.use(hotMiddleware); // 服务端 编译serverCompile(serverConfig, (serverBundleContent) => {serverBundle = serverBundleContent;updateClientAndServer();})return readyPromise;
}let devServerPromise = devServer((serverBundle, options) => {renderer = createBundleRenderer(serverBundle, Object.assign(options, {runInNewContext: false,}))
});

在devServer函数中,分别使用clientCompile函数,编译客户端。在回调函数把生成的clientManifestContent内容赋值给clientManifest,然后通知updateClientAndServer函数完成后续内容。同时clientCompile函数也返回了两个中间件并放入use函数中,完成后续的热更新和webpack dev server. 和clientCompile函数类似,serverCompile函数,它也是在回调函数中把生成的serverBundleContent赋值给serverBundle,并通知updateClientAndServer函数完成其他内容。在updateClientAndServer函数中,当clientManifest和serverBundle内容都有时,就可以把promise resolve掉,进而可以调用renderToString函数生成html字符串,发送给浏览器,最后实现ssr。

在devServer函数中还是用了tempWatch,该函数的作用是当模板文件发生变化时,更新相关内容。代码如下

// 路径 dev/tempWatch.cjs
let fs = require('fs');
let chokidar = require('chokidar');module.exports = function tempWatch(templatePath, watchCb) {// 监听模板html文件 changechokidar.watch(templatePath).on('change', () => {console.log('模板更新中...');templateContent = fs.readFileSync(templatePath, 'utf-8');console.log('模板更新成功!');// 更新模块watchCb();});
}

到此为止,vue ssr的配置和生成基本结束。但是现在通过浏览器还是访问不了,还需要最后一步配置服务器

配置服务器提供访问

在dev.cjs中添加路由配置,然后导处路由,在程序入口处使用。

// 路径 router/dev.cjs
const SERVER = require('express');
const ROUTER = SERVER.Router();
const FS = require('fs');
const PATH = require('path');
let clientConfig = require('../../build/webpack.client.dev');
let serverConfig = require('../../build/webpack.server.dev'); 
let templatePath =  PATH.resolve(__dirname, '../server.template.html');
let { createBundleRenderer } = require('vue-server-renderer')
let serverCompile = require('../dev/serverCompile.cjs');
let clientCompile = require('../dev/clientCompile.cjs');
let tempWatch = require('../dev/tempWatch.cjs');let renderer;const devServer = (cb) => {let clientManifest, serverBundle, readyResolve, templateContent;templateContent = FS.readFileSync(templatePath, 'utf-8');let readyPromise = new Promise(resolve => readyResolve = resolve );// 更新客户端和服务端内容let updateClientAndServer = () => {// 只有构建清单文件都存在时,执行更新操作if(clientManifest && serverBundle) {readyResolve(); // 把promise resolve掉cb(serverBundle, {template: templateContent,clientManifest})}};// 监听模板文件tempWatch(templatePath, () => {updateClientAndServer();});// 客户端 编译let { devMiddleware, hotMiddleware} = clientCompile(clientConfig, (clientManifestContent) => {clientManifest = clientManifestContent;updateClientAndServer();})ROUTER.use(devMiddleware);ROUTER.use(hotMiddleware); // 服务端 编译serverCompile(serverConfig, (serverBundleContent) => {serverBundle = serverBundleContent;updateClientAndServer();})return readyPromise;
}let devServerPromise = devServer((serverBundle, options) => {renderer = createBundleRenderer(serverBundle, Object.assign(options, {runInNewContext: false,}))
});ROUTER.get('*', (req, res) => {const context = {url: req.url}devServerPromise.then(() => {renderer.renderToString(context).then(html => {res.send(html)}).catch(err => {console.log('err',req.url,err)})})
})module.exports = ROUTER;

在程序入口处使用该路由


// 路径 server/index.cjs
const SERVER = require('express')();
const SSRROUTER = require('./router/dev.cjs');
const PORT = 8000;SERVER.use(SSRROUTER);SERVER.listen(PORT,() => {console.log(`app listening at port ${PORT}`);
});

最后输入npm run dev启动项目,结果如下

请添加图片描述

注: 配置ssr的过程有点繁琐,如果途中有配置错的地方可以查看我的github ssr demo

如果博客中有什么不理解的或者错误内容,欢迎指出,及时更正

相关文章:

手动创建 vue2 ssr 开发环境

本文和个人博客同步发表 更多优质文章查看个人博客 前言 手动搭建 vue ssr 一直是一些前端开发者的噩梦&#xff0c;因为其中牵扯到很多依赖包之间的配置以及webpack在node中的使用。就拿webpack配置来说&#xff0c;很多前端开发者还是喜欢用webpack-cli脚手架搭建项目。导致…...

RHCE-操作系统刻录工具

Windows 1.准备材料。 一个可用的windows操作系统(下载的时候用迅雷比较快) MSDN, 我告诉你 - 做一个安静的工具站 大于等于8G的U盘 想要安装的系统光盘镜像 U盘烧录工具&#xff08;软碟通&#xff09; UltraISO软碟通中文官方网站 - 光盘映像文件制作/编辑/转换工具 …...

PHP面向对象01:面向对象基础

PHP面向对象01&#xff1a;面向对象基础一、关键字说明二、技术实现1. 定义类2. 类成员三、 访问修饰限定符1. public2. protected3. private4. 空修饰限定符四、类内部对象五、构造和析构1. 构造方法2. 析构方法六、范围解析操作符1. 访问类常量2. 静态成员3. self关键字七、类…...

《爆肝整理》保姆级系列教程python接口自动化(十八)--重定向(Location)(详解)

简介   在实际工作中&#xff0c;有些接口请求完以后会重定向到别的url&#xff0c;而你却需要重定向前的url。URL主要是针对虚拟空间而言&#xff0c;因为不是自己独立管理的服务器&#xff0c;所以无法正常进行常规的操作。但是自己又不希望通过主域名的二级目录进行访问&…...

MySQL的索引、视图

什么是索引模式(schema)中的一个数据库对象 在数据库中用来加速对表的查询 通过使用快速路径访问方法快速定位数据,减少了磁盘的I/O 与表独立存放&#xff0c;但不能独立存在&#xff0c;必须属于某个表 由数据库自动维护&#xff0c;表被删除时&#xff0c;该表上的索引自动被…...

【JavaWeb】网络层协议——IP协议

目录 IP协议结构 IP地址管理 特殊IP 解决IP地址不够用 动态分配IP地址 NAT网络地址转换 IPV6 IP协议结构 版本&#xff1a;就是IP协议的版本号。目前只有 4 和 6。这里介绍的是IPV4 首部长度&#xff1a;单位是4字节。于TCP首部长度完全一致&#xff0c;也是可变的&…...

【Python学习笔记】41.Python3 多线程

前言 本章介绍Python的多线程。 Python3 多线程 多线程类似于同时执行多个不同程序&#xff0c;多线程运行有如下优点&#xff1a; 使用线程可以把占据长时间的程序中的任务放到后台去处理。用户界面可以更加吸引人&#xff0c;比如用户点击了一个按钮去触发某些事件的处理…...

Windows 版本ffmpeg编译概述

在使用ffmpeg过程当中&#xff0c;ffmpeg在Linux(包括mac,android)编译非常容易,直接configure,make即可&#xff0c;Android需要交叉编译,在windows就比较麻烦&#xff0c;庆幸的是ffmpeg官方提供已编译好Windows版本的二进制库&#xff08;http://ffmpeg.org/download.html#b…...

NETCore下CI/CD之自动化测试 (详解篇)

NETCore下CI/CD之自动化测试 &#xff08;详解篇&#xff09; 目录&#xff1a;导读 前言 安装JDK 安装 Tomcat 首先&#xff0c;我们需要指定 Tomcat.PID 进程文件&#xff0c;进入 /usr/local/tomcat/bin&#xff0c;编辑文件 增加 tomcat 账户并赋予权限 防止Jeknins…...

Hoeffding不等式剪枝方法

在基于物品的协通过滤算法中&#xff0c;当用户历史行为数据有很多时&#xff0c;对计算会有很大挑战&#xff0c;对此可以使用剪枝对数据进行化简来达到减少计算量。     不是每个物品对都需要进行增量计算。对于两个物品的相似度&#xff0c;每次更新都能够得到一个新的相…...

【算法】数组中的重复数字问题

数组中的重复数据 数组中重复的数字 错误的集合 以第三题&#xff0c;错误的集合为例 对于这样的问题&#xff0c;有很简单的解决方式&#xff0c;先遍历一次数组&#xff0c;用一个哈希表记录每个数字出现的次数&#xff0c;然后遍历一次 [1…N]&#xff0c;看看那个元素重…...

数值方法笔记2:解决非线性方程

1. 不动点定理及其条件验证2. 收敛阶、收敛检测与收敛加速2.1 如何估计不动点迭代的收敛阶xk1g(xk){x}_{{k}1}{g}\left({x}_{{k}}\right)xk1​g(xk​)2.2 给定精度的情况下&#xff0c;如何预测不动点迭代需要迭代的次数2.3 如何加快收敛的速度2.4 停止不定点迭代的条件2.5 不动…...

基于SpringBoot的在线文档管理系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏…...

软件体系结构(期末复习)

文章目录软件体系结构软件体系结构概论软件体系结构建模软件体系结构风格统一建模语言基于体系结构的软件开发软件体系结构 软件体系结构概论 软件危机是指计算机软件的开发和维护过程中遇到的一系列严重问题。 软件危机的表现: 软件危机的原因: 软件工程的基本要素&#xf…...

[vue3] pinia的基本使用

使用Pinia npm install piniastore文件里index.js import { createPinia } from piniaconst pinia createPinia()export default piniamain.js导入并引用 import { createApp } from vue import App from ./App.vue import pinia from ./storescreateApp(App).use(pinia).m…...

进程和线程详解

在计算机领域中&#xff0c;进程和线程是非常重要的概念。了解进程和线程是软件开发的基础&#xff0c;也是计算机科学教育中的一部分。本文将介绍进程和线程的概念、区别和应用。 一、什么是进程 在计算机科学中&#xff0c;进程是正在执行的程序实例。一个进程可以由一个或…...

《刀锋》读书笔记

刀锋&#xff08;毛姆长篇作品精选&#xff09;毛姆50个笔记点评认为好看的确是完美的结局。《刀锋》里面的人每个人都以自己的方式生活着。艾略特的势利&#xff0c;拉里的自由&#xff0c;伊莎贝尔的现实&#xff0c;苏珊的清醒&#xff0c;索菲的堕落&#xff0c;至于“我”…...

nginx中的ngx_modules

ngx_modules和ngx_module_names是configure脚本生成的&#xff0c;是在objs/ngx_modules.c文件中与其生成的相关的脚本文件相关的变量在options脚本中定义了objs目录的变量NGX_OBJSobjs在init脚本中定义的最终存放ngx_modules的文件 NGX_MODULES_C$NGX_OBJS/ngx_modules.c2. 处…...

设计模式之访问者模式

什么是访问者模式 访问者模式提供了一个作用于某对象结构中的各元素的操作表示&#xff0c;他使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。     访问者模式主要包含以下几个角色&#xff1a;         Vistor(抽象访问者)&#xff1a;为对象结…...

Go项目(三)

文章目录用户微服务表结构查表web 服务跨域问题图形验证码短信用户注册服务中心注册 grpc 服务动态获取端口负载均衡配置中心启动项目小结用户微服务 作为系统的第一个微服务&#xff0c;开发的技术点前面已经了解了一遍&#xff0c;虽有待补充&#xff0c;但急需实战这里主要…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻

在如今就业市场竞争日益激烈的背景下&#xff0c;越来越多的求职者将目光投向了日本及中日双语岗位。但是&#xff0c;一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧&#xff1f;面对生疏的日语交流环境&#xff0c;即便提前恶补了…...

相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)

【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...

网站指纹识别

网站指纹识别 网站的最基本组成&#xff1a;服务器&#xff08;操作系统&#xff09;、中间件&#xff08;web容器&#xff09;、脚本语言、数据厍 为什么要了解这些&#xff1f;举个例子&#xff1a;发现了一个文件读取漏洞&#xff0c;我们需要读/etc/passwd&#xff0c;如…...

Golang——7、包与接口详解

包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...

实战设计模式之模板方法模式

概述 模板方法模式定义了一个操作中的算法骨架&#xff0c;并将某些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的前提下&#xff0c;重新定义算法中的某些步骤。简单来说&#xff0c;就是在一个方法中定义了要执行的步骤顺序或算法框架&#xff0c;但允许子类…...

基于 HTTP 的单向流式通信协议SSE详解

SSE&#xff08;Server-Sent Events&#xff09;详解 &#x1f9e0; 什么是 SSE&#xff1f; SSE&#xff08;Server-Sent Events&#xff09; 是 HTML5 标准中定义的一种通信机制&#xff0c;它允许服务器主动将事件推送给客户端&#xff08;浏览器&#xff09;。与传统的 H…...

基于Python的气象数据分析及可视化研究

目录 一.&#x1f981;前言二.&#x1f981;开源代码与组件使用情况说明三.&#x1f981;核心功能1. ✅算法设计2. ✅PyEcharts库3. ✅Flask框架4. ✅爬虫5. ✅部署项目 四.&#x1f981;演示效果1. 管理员模块1.1 用户管理 2. 用户模块2.1 登录系统2.2 查看实时数据2.3 查看天…...

【Zephyr 系列 16】构建 BLE + LoRa 协同通信系统:网关转发与混合调度实战

🧠关键词:Zephyr、BLE、LoRa、混合通信、事件驱动、网关中继、低功耗调度 📌面向读者:希望将 BLE 和 LoRa 结合应用于资产追踪、环境监测、远程数据采集等场景的开发者 📊篇幅预计:5300+ 字 🧭 背景与需求 在许多 IoT 项目中,单一通信方式往往难以兼顾近场数据采集…...

Vue3学习(接口,泛型,自定义类型,v-for,props)

一&#xff0c;前言 继续学习 二&#xff0c;TS接口泛型自定义类型 1.接口 TypeScript 接口&#xff08;Interface&#xff09;是一种定义对象形状的强大工具&#xff0c;它可以描述对象必须包含的属性、方法和它们的类型。接口不会被编译成 JavaScript 代码&#xff0c;仅…...

C++ 变量和基本类型

1、变量的声明和定义 1.1、变量声明规定了变量的类型和名字。定义初次之外&#xff0c;还申请存储空间&#xff0c;也可能会为变量赋一个初始值。 如果想声明一个变量而非定义它&#xff0c;就在变量名前添加关键字extern&#xff0c;而且不要显式地初始化变量&#xff1a; e…...