服務端渲染與 Universal React App

隨著 Webpack 等前端構建工具的普及,客戶端渲染因為其構建方便,部署簡單等方面的優勢,逐漸成為了現代網站的主流渲染模式。而在剛剛發布的 React v16.0 中,改進后更為優秀的服務端渲染性能作為六大更新點之一,被 React 官方重點提及。為此筆者還專門做了一個小調查,分別詢問了二十位國內外(國內國外各十位)前端開發者,希望能夠了解一下服務端渲染在使用 React 公司中所占的比例。

出人意料的是,十位國內的前端開發者中在生產環境使用服務端渲染的只有三位。而在國外的十位前端開發者中,使用服務端渲染的達到了驚人的八位。

這讓人不禁開始思考,同是 React 的深度使用者,為什么國內外前端開發者在服務端渲染這個 React 核心功能的使用率上有著如此巨大的差別?在經過又一番刨根問底地詢問后,真正的答案逐漸浮出水面,那就是可靠的 SEO(reliable SEO)。

相比較而言,國外公司對于 SEO 的重視程度要遠高于國內公司,在這方面積累的經驗也要遠多于國內公司,前端頁面上需要服務端塞入的內容也絕不僅僅是用戶所看到的那些而已。所以對于國外的前端開發者來說,除去公司內部系統不談,所有的客戶端應用都需要做大量的 SEO 工作,服務端渲染也就順理成章地成為了一個必選項。這也從一個側面證明了國內外互聯網環境的一個巨大差異,即雖然國際上也有諸如 Google,Facebook,Amazon 這樣的巨頭公司,但放眼整個互聯網,這些巨頭公司所產生的黑洞效應并沒有國內 BAT 三家那樣如此得明顯,中小型公司依然有其生存的空間,搜索引擎所帶來的自然流量就足夠中小型公司可以活得很好。在這樣的前提下,SEO 的重要性自然也就不言而喻了。

除去 SEO,服務端渲染對于前端應用的首屏加載速度也有著質的提升。特別是在 React v16.0 發布之后,新版 React 的服務端渲染性能相較于老版提升了三倍之多,這讓已經在生產環境中使用服務端渲染的公司“免費”獲得了一次網站加載速度提升的機會,同時也吸引了許多還未在生產環境中使用服務端渲染的開發者。

客戶端渲染 vs. 服務端渲染 vs. 同構

在深入服務端渲染的細節之前,讓我們先明確幾個概念的具體含義。

  • 客戶端渲染:頁面在 JavaScript,CSS 等資源文件加載完畢后開始渲染,路由為客戶端路由,也就是我們經常談到的 SPA(Single Page Application)。
  • 服務端渲染:頁面由服務端直接返回給瀏覽器,路由為服務端路由,URL 的變更會刷新頁面,原理與 ASP,PHP 等傳統后端框架類似。
  • 同構:英文表述為 Isomorphic 或 Universal,即編寫的 JavaScript 代碼可同時運行在瀏覽器及 Node.js 兩套環境中,用服務端渲染來提升首屏的加載速度,首屏之后的路由由客戶端控制,即在用戶到達首屏后,整個應用仍是一個 SPA。

在明確了這三種渲染方案的具體含義后,我們可以發現,不論是客戶端渲染還是服務端渲染,都有著其明顯的缺陷,而同構顯然是結合了二者優點之后的一種更好的解決方案。

但想在客戶端寫出一套完全符合同構要求的 React 代碼并不是一件容易的事,與此同時還需要額外部署一套穩定的服務端渲染服務,這二者相加起來的開發或遷移成本都足以擊潰許多想要嘗試服務端渲染的 React 開發者的信心。

那么今天就讓我們來一起總結下,符合同構要求的 React 代碼都有哪些需要注意的地方,以及如何搭建起一個基礎的服務端渲染服務。

總體架構

為了方便各位理解同構的具體實現過程,筆者基于 reactreact-routerredux 以及 webpack3 實現了一個簡單的腳手架項目,支持客戶端渲染和服務端渲染兩種開發方式,供各位參考。

  1. 服務端預先獲取編譯好的客戶端代碼及其他資源。
  2. 服務端接收到用戶的 HTTP 請求后,觸發服務端的路由分發,將當前請求送至服務端渲染模塊處理。
  3. 服務端渲染模塊根據當前請求的 URL 初始化 memory history 及 redux store。
  4. 根據路由獲取渲染當前頁面所需要的異步請求(thunk)并獲取數據。
  5. 調用 renderToString 方法渲染 HTML 內容并將初始化完畢的 redux store 塞入 HTML 中,供客戶端渲染時使用。
  6. 客戶端收到服務端返回的已渲染完畢的 HTML 內容并開始同步加載客戶端 JavaScript,CSS,圖片等其他資源。
  7. 之后的流程與客戶端渲染完全相同,客戶端初始化 redux store,路由找到當前頁面的組件,觸發組件的生命周期函數,再次獲取數據。唯一不同的是 redux store 的初始狀態將由服務端在 HTML 中塞入的數據提供,以保證客戶端渲染時可以得到與服務端渲染相同的結果。受益于 Virtual DOM 的 diff 算法,這里并不會觸發一次冗余的客戶端渲染。

在了解了同構的大致思路后,接下來再讓我們對同構中需要注意的點逐一進行分析,與各位一起探討同構的最佳實踐。

客戶端與服務端構建過程不同

因為運行環境與渲染目的的不同,共用一套代碼的客戶端與服務端在構建方面有著許多的不同之處。

入口(entry)不同

客戶端的入口為 ReactDOM.render 所在的文件,即將根組件掛載在 DOM 節點上。而服務端因為沒有 DOM 的存在,只需要拿到需要渲染的 react 組件即可。為此我們需要在客戶端抽離出獨立的 createAppcreateStore 的方法。

// createApp.jsimport React from 'react';
import { Provider } from 'react-redux';
import Router from './router';const createApp = (store, history) => (<Provider store={store}><Router history={history} /></Provider>
);export default createApp;復制代碼
// createStore.jsimport { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import { routerReducer, routerMiddleware } from 'react-router-redux';
import reduxThunk from 'redux-thunk';
import reducers from './reducers';
import routes from './router/routes';function createAppStore(history, preloadedState = {}) {// enhancerslet composeEnhancers = compose;if (typeof window !== 'undefined') {// eslint-disable-next-line no-underscore-danglecomposeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;}// middlewaresconst routeMiddleware = routerMiddleware(history);const middlewares = [routeMiddleware,reduxThunk,];const store = createStore(combineReducers({...reducers,router: routerReducer,}),preloadedState,composeEnhancers(applyMiddleware(...middlewares)),);return {store,history,routes,};
}export default createAppStore;復制代碼

并在 app 文件夾中將這兩個方法一起輸出出去:

import createApp from './createApp';
import createStore from './createStore';export default {createApp,createStore,
};復制代碼

出口(output)不同

為了最大程度地提升用戶體驗,在客戶端渲染時我們將根據路由對代碼進行拆分,但在服務端渲染時,確定某段代碼與當前路由之間的對應關系是一件非常繁瑣的事情,所以我們選擇將所有客戶端代碼打包成一個完整的 js 文件供服務端使用。

理想的打包結果如下:

├── build
│   └── v1.0.0
│       ├── assets
│       │   ├── 0.0.257727f5.js
│       │   ├── 0.0.257727f5.js.map
│       │   ├── 1.1.c3d038b9.js
│       │   ├── 1.1.c3d038b9.js.map
│       │   ├── 2.2.b11f6092.js
│       │   ├── 2.2.b11f6092.js.map
│       │   ├── 3.3.04ff628a.js
│       │   ├── 3.3.04ff628a.js.map
│       │   ├── client.fe149af4.js
│       │   ├── client.fe149af4.js.map
│       │   ├── css
│       │   │   ├── style.db658e13004910514f8f.css
│       │   │   └── style.db658e13004910514f8f.css.map
│       │   ├── images
│       │   │   └── 5d5d9eef.svg
│       │   ├── vendor.db658e13.js
│       │   └── vendor.db658e13.js.map
│       ├── favicon.ico
│       ├── index.html
│       ├── manifest.json
│       └── server (服務端需要的資源將被打包至這里)
│           ├── assets
│           │   ├── server.4b6bcd12.js
│           │   └── server.4b6bcd12.js.map
│           └── manifest.json
復制代碼

使用的插件(plugin)不同

與客戶端不同,除去 JavaScript 之外,服務端并不需要任何其他的資源,如 HTML 及 CSS 等,所以在構建服務端 JavaScript 時,諸如 HtmlWebpackPlugin 等客戶端所特有的插件就可以省去了,具體細節各位可以參考項目中的 webpack.config.js。

數據獲取方式不同

異步數據獲取一直都是服務端渲染做得不夠優雅的一個地方,其主要問題在于無法直接復用客戶端的數據獲取方法。如在 redux 的前提下,服務端沒有辦法像客戶端那樣直接在組件的componentDidMount 中調用 action 去獲取數據。

為了解決這一問題,我們針對每一個 view 為其抽象出了一個 thunk 文件,并將其綁定在客戶端的路由文件中。這樣我們就可以在服務端通過 react-router-config 提供的 matchRoutes 方法找到當前頁面的 thunk,并在 renderToString 之前 dispatch 這些異步方法,將數據更新至 redux store 中,以保證 renderToString 的渲染結果是包含異步數據的。

// thunk.js
import homeAction from '../home/action';
import action from './action';const thunk = store => ([store.dispatch(homeAction.getMessage()),store.dispatch(action.getUser()),
]);export default thunk;// createAsyncThunk.js
import get from 'lodash/get';
import isArrayLikeObject from 'lodash/isArrayLikeObject';function promisify(value) {if (typeof value.then === 'function') {return value;}if (isArrayLikeObject(value)) {return Promise.all(value);}return value;
}function createAsyncThunk(thunk) {return store => (thunk().then(component => get(component, 'default', component)).then(component => component(store)).then(component => promisify(component)));
}export default createAsyncThunk;// routes.js
const routes = [{path: '/',exact: true,component: AsyncHome,thunk: createAsyncThunk(() => import('../../views/home/thunk')),
}, {path: '/user',component: AsyncUser,thunk: createAsyncThunk(() => import('../../views/user/thunk')),
}];復制代碼

服務端核心的頁面渲染模塊:

const ReactDOM = require('react-dom/server');
const { matchRoutes } = require('react-router-config');
const { Helmet } = require('react-helmet');
const serialize = require('serialize-javascript');
const createHistory = require('history/createMemoryHistory').default;
const get = require('lodash/get');
const head = require('lodash/head');
const { getClientInstance } = require('../client');// Initializes the store with the starting url = require( request.
function configureStore(req, client) {console.info('server path', req.originalUrl);const history = createHistory({ initialEntries: [req.originalUrl] });const preloadedState = {};return client.app.createStore(history, preloadedState);
}// This essentially starts passing down the "context"
// object to the Promise "then" chain.
function setContextForThenable(context) {return () => context;
}// Prepares the HTML string and the appropriate headers
// and subequently string replaces them into their placeholders
function renderToHtml(context) {const { client, store, history } = context;const appObject = client.app.createApp(store, history);const appString = ReactDOM.renderToString(appObject);const helmet = Helmet.renderStatic();const initialState = serialize(context.store.getState(), {isJSON: true});context.renderedHtml = client.html().replace(/<!--appContent-->/g, appString).replace(/<!--appState-->/g, `<script>window.__INITIAL_STATE__ = ${initialState}</script>`).replace(/<\/head>/g, [helmet.title.toString(),helmet.meta.toString(),helmet.link.toString(),'</head>',].join('\n')).replace(/<html>/g, `<html ${helmet.htmlAttributes.toString()}>`).replace(/<body>/g, `<body ${helmet.bodyAttributes.toString()}>`);return context;
}// SSR Main method
// Note: Each function in the promise chain beyond the thenable context
// should return the context or modified context.
function serverRender(req, res) {const client = getClientInstance(res.locals.clientFolders);const { store, history, routes } = configureStore(req, client);const branch = matchRoutes(routes, req.originalUrl);const thunk = get(head(branch), 'route.thunk', () => {});Promise.resolve(null).then(thunk(store)).then(setContextForThenable({ client, store, history })).then(renderToHtml).then((context) => {res.send(context.renderedHtml);return context;}).catch((err) => {console.error(`SSR error: ${err}`);});
}module.exports = serverRender;復制代碼

在客戶端,我們可以直接在 componentDidMount 中調用這些 action:

const mapDispatchToProps = {getUser: action.getUser,getMessage: homeAction.getMessage,
};componentDidMount() {this.props.getMessage();this.props.getUser();
}復制代碼

在分離了服務端與客戶端 dispatch 異步請求的方式后,我們還可以針對性地對服務端的 thunk 做進一步的優化,即只請求首屏渲染需要的數據,剩下的數據交給客戶端在 js 加載完畢后再請求。

但這里又引出了另一個問題,比如在上面的例子中,getUser 和 getMessage 這兩個異步請求分別在服務端與客戶端各請求了一次,即我們在很短的時間內重復請求了同一個接口兩次,這是可以避免的嗎?

這樣的數據獲取方式在純服務端渲染時自然是冗余的,但在同構的架構下,其實是無法避免的。因為我們并不知道用戶在訪問客戶端的某個頁面時,是從服務端路由來的(即首屏),還是從客戶端路由(首屏之后的后續路由)來的。也就是說如果我們不在組件的 componentDidMount 中去獲取異步數據的話,一旦用戶到達了某個頁面,再點擊頁面中的某個元素跳轉至另一頁面時,是不會觸發服務端的數據獲取的,因為這時走的實際上是客戶端路由。

服務端渲染還能做些什么

除去 SEO 與首屏加速,在額外部署了一套服務端渲染服務后,我們當然希望它能為我們分擔更多的事情,那么究竟有哪些事情放在服務端去做是更為合適的呢?筆者總結了以下幾點。

初始化應用狀態

除去獲取當前頁面的數據,在做了同構之后,客戶端還可以將獲取應用全局狀態的一些請求也交由服務端去做,如獲取當前時區,語言,設備信息,用戶等通用的全局數據。這樣客戶端在初始化 redux store 時就可以直接獲取到上述數據,從而加快其他頁面的渲染速度。與此同時,在分離了這部分業務邏輯到服務端之后,客戶端的業務邏輯也會變得更加清晰。當然,如果你想做一個純粹的 Universal App,也可以把初始化應用狀態封裝成一個方法,讓服務端與客戶端都可以自由地去調用它。

更早的路由處理

相較于客戶端,服務端可以更早地對當前 URL 進行一些業務邏輯上的判斷。比如 404 時,服務端可以直接將另一個 error.html 的模板發送至客戶端,用戶也就可以在第一時間收到相應的反饋,而不需要等到所有 JavaScript 等客戶端資源加載完畢之后,才看到由客戶端渲染的 404 頁面。

Node.js 中間層

有了服務端渲染這一層后,服務端還可以幫助客戶端向 Cookie 中注入一些后端 API 中沒有的數據,甚至做一些接口聚合,數據格式化的工作。這時,我們所寫的 Node.js 服務端就不再是一個單純的渲染服務了,而是進化為了一個 Node.js 中間層,可以幫助客戶端完成許多在客戶端做不到或很難做到的事情。

要不要做同構

在分析了同構的具體實現細節并了解了同構的好處之后,我們也需要知道這一切的好處并不是沒有代價的,同構或者說服務端渲染最大的瓶頸就是服務端的性能。

在用戶規模大到一定程度之后,客戶端渲染本身就是一個完美的分布式系統,我們可以充分地利用用戶的電腦去運行 JavaScript 中那些復雜的運算,而服務端渲染卻將這些工作全部攬了回來并加到了網站自己的服務器上。

所以,考慮到投入產出比,同構可能并不適用于前端需要大量計算(如包含大量圖表的頁面)且用戶量非常巨大的應用,卻非常適用于大部分的內容展示型網站,比如知乎就是一個很好的例子。以知乎為例,服務端渲染與客戶端渲染的成本幾乎是相同的,重點都在于獲取用戶時間線上的數據,這時多頁面的服務端渲染可以很好地加快首屏渲染的速度,又因為運行 renderToString 時的計算量并不大,即使用戶量很大,也仍然是一件值得去做的事情。

小結

結合之前文章中提到的前端數據層的概念,服務端渲染服務其實是一個很好的前端開發介入服務端開發的切入點,在完成了服務端渲染服務后,對數據接口做一些代理或整合也是非常值得去嘗試的工作。

一個代碼庫之所以復雜,很多時候就是因為分層架構沒有做好而導致其中某一個模塊過于臃腫,集中了大部分的業務復雜度,但其他模塊又根本幫不上忙。想要做好前端數據層的工作,只把眼光局限在客戶端是遠遠不夠的,將業務復雜度均分到客戶端及服務端,并讓兩方分別承擔各自適合的工作,可能會是一種更好的解法。


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/454395.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/454395.shtml
英文地址,請注明出處:http://en.pswp.cn/news/454395.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

zxing .net 多種條碼格式的生成

下載地址&#xff1a;http://zxingnet.codeplex.com/ zxing.net是.net平臺下編解條形碼和二維碼的工具&#xff0c;使用非常方便。 本文主要說明一下多種類型條碼的生成。 適用的場景&#xff0c;標簽可視化設計時&#xff0c;自定義條碼類型&#xff0c;預覽。 遍歷zxing支持的…

k8s dashboard_windows10 部署 docker+k8s 集群

下面是經過踩坑之后的 windows10 單機部署 docker k8s 步驟&#xff0c;其中有幾處比較關鍵的地方需要注意&#xff0c;后面加粗標注&#xff0c;下面就開始吧&#xff01;0、下載cmder在 windows 上有一個趁手的命令行工具非常有必要&#xff0c;推薦 Cmder&#xff0c;下面是…

Python 之網絡編程基礎

套接字&#xff08;socket&#xff09;初使用 基于TCP協議的socket tcp是基于鏈接的&#xff0c;必須先啟動服務端&#xff0c;然后再啟動客戶端去鏈接服務端 server端 import socket sk socket.socket() sk.bind((127.0.0.1,8898)) # 把地址綁定到套接字 sk.listen() …

ajax on ture,細數Ajax請求中的async:false和async:true的差異

實例如下&#xff1a;function test(){var temp"00";$.ajax({async: false,type : "GET",url : userL_checkPhone.do,complete: function(msg){alert(complete);},success : function(data) {alert(success);tempdata;temp"aa";}});alert(temp);…

阿里云郵箱登錄日志中有異地IP登錄是怎么回事?該怎么辦?

注意&#xff0c;請先到阿里云官網 領取幸運券&#xff0c;除了價格上有很多優惠外&#xff0c;還可以參與抽獎。詳見&#xff1a;https://promotion.aliyun.com/ntms/act/ambassador/sharetouser.html?userCode2a7uv47d&utm_source2a7uv47d以下可能&#xff1a;1、您的郵…

面試之網絡編程和并發

1、簡述 OSI 七層協議。 物理層&#xff1a;主要基于電器特性發送高低電壓(1、0)&#xff0c;設備有集線器、中繼器、雙絞線等&#xff0c;單位&#xff1a;bit 數據鏈路層&#xff1a;定義了電信號的分組方式&#xff0c;設備&#xff1a;交換機、網卡、網橋&#xff0c;單位&…

redis 遠程主機強迫關閉了一個現有的連接_記一次Redis+Getshell經驗分享

你是我患得患失的夢&#xff0c;我是你可有可無的人&#xff0c;畢竟這穿越山河的箭&#xff0c;刺的都是用情之疾的人。前言&#xff1a;當我們接到一個授權滲透測試的時候&#xff0c;常規漏洞如注入、文件上傳等嘗試無果后&#xff0c;掃描端口可能會發現意外收獲。知己知彼…

無線連接 服務器,服務器無線遠程連接

服務器無線遠程連接 內容精選換一換華為云幫助中心&#xff0c;為用戶提供產品簡介、價格說明、購買指南、用戶指南、API參考、最佳實踐、常見問題、視頻幫助等技術文檔&#xff0c;幫助您快速上手使用華為云服務。使用Mac版Microsoft Remote Desktop工具&#xff0c;遠程連接W…

面試前您該做的事情

選自本人作品&#xff1a;《軟件性能測試與LR實戰》 無論您是剛剛畢業的大學生朋友&#xff0c;還是已經有工作經驗的同行&#xff0c;大家都不可避免的面臨一個問題就是找工作或者換工作的問題。在整個應聘過程中&#xff0c;面試無疑是最具有決定性意義的重要環節&#xff0c…

IO模型

IO模型介紹 傳統的網絡IO模型包括五種&#xff1a; blocking IO 阻塞IOnonblocking IO 非阻塞IOIO multiplexing IO多路復用signal driven IO 信號驅動IOasynchronous IO 異步IO 由于signal driven IO&#xff08;信號驅動IO&#xff09;在實際中…

重溫數據結構:樹 及 Java 實現(轉)

轉自&#xff1a;http://blog.csdn.net/u011240877/article/details/53193877 讀完本文你將了解到&#xff1a; 什么是樹樹的相關術語 根節點父親節點孩子節點葉子節點如上所述節點的度樹的度節點的層次樹的高度樹的深度樹的兩種實現 數組表示鏈表表示的節點樹的幾種常見分類及…

Powershell檢測AD賬戶密碼過期時間并郵件通知

腳本主要實現了兩個功能 &#xff1a; 一能判斷賬戶密碼的過期時間并通過郵件通知到賬戶&#xff1b; 二是將這些即將過期的賬戶信息累計通知到管理員。 腳本如下&#xff1a; 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051…

js list刪除指定元素_vue.js

vue.js 中M V MV代表哪一部分 <插值表達式&#xff08;v-cloak v-text v-html v-bind&#xff08;縮寫是:&#xff09; v-on&#xff08;縮寫是&#xff09; v-model v-for v-if v-show &#xff09;<body><div id"app"><!-- 使用 v-cloak 能夠解決…

修改db2管理服務器,創建DB2管理服務器的兩種情況

DB2管理服務器在創建時分為創建一個和創建多個兩種情況&#xff0c;下面就為您詳細介紹這兩種創建DB2管理服務器的情況&#xff0c;供您參考學習。一、創建DB2管理服務器(只能創建一個)1、首先創建管理服務組用戶(可不建)命令&#xff1a;sudo groupadd dasadm12、創建用戶命令…

系統程序員成長計劃-走近專業程序員

轉載時請注明出處和作者聯系方式 文章出處&#xff1a;http://www.limodev.cn/blog 作者聯系方式&#xff1a;李先靜 <xianjimli at hotmail dot com> 需求簡述 用C語言編寫一個雙向鏈表。如果你有一定的C語言編程經驗&#xff0c;這自然是小菜一碟。有的讀者可能連一個…

Python 內置模塊之 asyncio(異步iO)

python3.0&#xff0c;標準庫里的異步網絡模塊&#xff1a;select(非常底層) &#xff0c;第三方異步網絡庫&#xff1a;Tornado&#xff0c;gevent python3.4&#xff0c;asyncio&#xff1a;支持 TCP &#xff0c;子進程 現在的asyncio&#xff0c;有了很多的模塊已經在支持…

前端js文件合并三種方式

最近在思考前端js文件該如何合并&#xff0c;當然不包括不能合并文件&#xff0c;而是我們能合并的文件&#xff0c;想了想應該也只有三種方式。 三個方式如下&#xff1a; 1. 一個大文件&#xff0c;所有js合并成一個大文件&#xff0c;所有頁面都引用它。 2. 各個頁面大文件&…

我們的系統檢測到您的計算機網絡中存在異常流量_如何建立我們的網絡防線?入侵檢測,確保我們的網絡安全...

目前我們的網絡安全趨勢日益嚴峻&#xff0c;那么如何利用入侵檢測系統確保我的網絡安全呢&#xff1f;入侵檢測又是什么呢&#xff1f;網絡安全入侵檢測技術是為保證計算機系統的安全&#xff0c;而設計與配置的一種能夠及時發現并報告系統中未授權或異常現象的技術&#xff0…

sql修改鏈接服務器名稱,SQL Server 創建鏈接服務器的腳本,自定義鏈路服務器的簡短名稱...

USE [master]GO/****** Object: LinkedServer [SQL01] Script Date: 2020/4/9 11:51:17 ******/EXEC master.dbo.sp_addlinkedserver server N‘SQL01‘, srvproductN‘‘, providerN‘SQLNCLI‘, datasrcN‘域名或者IP‘/* For security reasons the linked server remot…

mybatis $和#源代碼分析

JDBC中&#xff0c;主要使用兩種語句&#xff0c;一種是支持參數化和預編譯的PreparedStatement,支持原生sql,支持設置占位符&#xff0c;參數化輸入的參數&#xff0c;防止sql注入攻擊&#xff0c;在mybatis的mapper配置文件中&#xff0c;我們通過使用#和$告訴mybatis我們需要…