jango 模版渲染技術(shù)是 Django 框架用于生成 web 頁面的一部分,它將模板文件中的內(nèi)容與上下文數(shù)據(jù)一起渲染成最終的 HTML 頁面。模版引擎提供了一種安全和靈活的方式來組織和管理頁面結(jié)構(gòu)和內(nèi)容。
1.模版文件
* 模版文件以 Python 字面表示,其擴(kuò)展名為 ".html"。
* 可以在模板文件中使用 Markup 語言來定義文檔結(jié)構(gòu)和內(nèi)容。
* 還可以將變量和數(shù)據(jù)片段(如列表或字典)嵌入到模板中。
2. 文檔結(jié)構(gòu)
* 模版文件使用行內(nèi)和嵌套代碼來定義頁面結(jié)構(gòu)。
* 經(jīng)常使用的模板標(biāo)簽用于包含和渲染數(shù)據(jù)。
3. 字面渲染
* 您可以使用內(nèi)置函數(shù)和過濾器來格式化和渲染數(shù)據(jù)。
* 也可以使用定制的模板函數(shù)來簡化模板代碼。
4 vicissural設(shè)計(jì)
* 使用包含特定模板部分的文件來分解頁面。
* 避免在單一模板文件中嵌套過多層。
* 遵循 MVC 設(shè)計(jì)模式,將模版用于頁面結(jié)構(gòu)和內(nèi)容的渲染。
性能優(yōu)化
* 緩存模版以降低渲染時(shí)間。
* 利用分片渲染來生成大型頁面。
最佳實(shí)踐
* 保持模板文件大小適中。
* 避免在模版中嵌入大量代碼。
* 建立模版部分來重用代碼。
結(jié)論
Django 模版渲染是構(gòu)建美觀且話語豐富的 web 界面的重要組成部分。通過了解模版系統(tǒng)的基本概念和最佳實(shí)踐,您可以輕松構(gòu)建優(yōu)良的 Django 應(yīng)用程序。
者:Jiawei Pan
轉(zhuǎn)發(fā)鏈接:https://mp.weixin.qq.com/s/hDdF8rni6MX4U55l3DKLQw
者:京東零售 姜欣
關(guān)于服務(wù)器端渲染方案,之前只接觸了基于react的Next.js,最近業(yè)務(wù)開發(fā)vue用的比較多,所以調(diào)研了一下vue的服務(wù)器端渲染方案。 首先:長文預(yù)警,下文包括了兩種方案的實(shí)踐,沒有耐心的小伙伴可以直接跳到方案標(biāo)題下,down代碼體驗(yàn)一下。
簡單來說就是用戶第一次請(qǐng)求頁面時(shí),頁面上的內(nèi)容是通過服務(wù)器端渲染生成的,瀏覽器直接顯示服務(wù)端返回的完整html就可以,加快首屏顯示速度。
舉個(gè)栗子:
當(dāng)我們?cè)L問一個(gè)商品列表時(shí),如果使用客戶端渲染(csr),瀏覽器會(huì)加載空白的頁面,然后下載js文件,通過js在客戶端請(qǐng)求數(shù)據(jù)并渲染頁面。如果使用服務(wù)器端渲染(ssr),在請(qǐng)求商品列表頁面時(shí),服務(wù)器會(huì)獲取所需數(shù)據(jù)并將渲染后的HTML發(fā)送給瀏覽器,瀏覽器一步到位直接展示,而不用等待數(shù)據(jù)加載和渲染,提高用戶的首屏體驗(yàn)。
優(yōu)點(diǎn):
(1)更好的seo:抓取工具可以直接查看完全渲染的頁面。現(xiàn)在比較常用的交互是頁面初始展示 loading 菊花圖,然后通過異步請(qǐng)求獲取內(nèi)容,但是但抓取工具并不會(huì)等待異步完成后再行抓取頁面內(nèi)容。
(2)內(nèi)容到達(dá)更快:不用等待所有的 js 都完成下載并執(zhí)行,所以用戶會(huì)更快速地看到完整渲染的頁面。
缺點(diǎn):
(1)服務(wù)器渲染應(yīng)用程序,需要處于 Node.js server 運(yùn)行環(huán)境
(2)開發(fā)成本比較高
總結(jié):
總得來說,決定是否使用服務(wù)器端渲染,取決于具體的業(yè)務(wù)場景和需求。對(duì)于具有大量靜態(tài)內(nèi)容的簡單頁面,客戶端渲染更合適一些,因?yàn)樗梢愿斓丶虞d頁面。但是對(duì)于需要從服務(wù)器動(dòng)態(tài)加載數(shù)據(jù)的復(fù)雜頁面,服務(wù)器端渲染可能是一個(gè)更好的選擇,因?yàn)樗梢蕴岣哂脩舻氖灼馏w驗(yàn)和搜索引擎優(yōu)化。
下面進(jìn)入正文
git 示例demo地址
結(jié)論前置:不建議用,配置成本高
官網(wǎng)地址: https://v2.ssr.vuejs.org/zh/
首先要吐槽一下官網(wǎng),按官網(wǎng)教程比較難搞,目錄安排的不太合理,一頓操作項(xiàng)目都沒起來...
并且官網(wǎng)示例的構(gòu)建配置代碼是webpack4的,現(xiàn)在初始化項(xiàng)目后基本安裝的都是webpack5,有一些語法不同
npm init -y
yarn add vue vue-server-renderer -S
yarn add express -S
yarn add webpack webpack-cli friendly-errors-webpack-plugin vue-loader babel-loader @babel/core url-loader file-loader vue-style-loader css-loader sass-loader sass webpack-merge webpack-node-externals -D
yarn add clean-webpack-plugin @babel/preset-env -D
yarn add rimraf // 模擬linx的刪除命令,在build時(shí)先刪除dist
yarn add webpack-dev-middleware webpack-hot-middleware -D
yarn add chokidar -D //監(jiān)聽變化
yarn add memory-fs -D
yarn add nodemon -D
...實(shí)在太多,如有缺失可以在package.json中查找
另外:我現(xiàn)在用的"vue-loader": "^15.9.0"版本,之前用的是"vue-loader": "^17.0.1",報(bào)了一個(gè)styles的錯(cuò)
app.js
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
import { sync } from 'vuex-router-sync'
// 導(dǎo)出一個(gè)工廠函數(shù),用于創(chuàng)建新的
// 應(yīng)用程序、router 和 store 實(shí)例
export function createApp () {
// 創(chuàng)建 router 和 store 實(shí)例
const router=createRouter()
const store=createStore()
sync(store, router)
const app=new Vue({
router,
store,
render: h=> h(App)
})
return { app, router, store }
}
entry-client.js
import Vue from 'vue'
import { createApp } from './app'
Vue.mixin({
beforeMount () {
const { asyncData }=this.$options
if (asyncData) {
this.dataPromise=asyncData({
store: this.$store,
route: this.$route
})
}
}
})
const { app, router, store }=createApp()
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
router.onReady(()=> {
// 在初始路由 resolve 后執(zhí)行,
// 使用 `router.beforeResolve()`,以便確保所有異步組件都 resolve。
router.beforeResolve((to, from, next)=> {
const matched=router.getMatchedComponents(to)
const prevMatched=router.getMatchedComponents(from)
// 找出兩個(gè)匹配列表的差異組件
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')
})
entry-server.js
import { createApp } from './app'
export default context=> {
// 返回一個(gè)promise,服務(wù)器能夠等待所有的內(nèi)容在渲染前,已經(jīng)準(zhǔn)備就緒,
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 })
}
// 對(duì)所有匹配的路由組件調(diào)用 `asyncData()`
Promise.all(matchedComponents.map(Component=> {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute
})
}
})).then(()=> {
context.state=store.state
resolve(app)
}).catch(reject)
}, reject)
})
}
其中一個(gè)非常重要的api:createBundleRenderer,這個(gè)api上面有一個(gè)方法renderToString將代碼轉(zhuǎn)化成html字符串,主要功能就是把用webpack把打包后的服務(wù)端代碼渲染出來。具體了解可看官網(wǎng)bundle renderer指引。
// server.js
const app=require('express')()
const { createBundleRenderer }=require('vue-server-renderer')
const fs=require('fs')
const path=require('path')
const resolve=file=> path.resolve(__dirname, file)
const isProd=process.env.NODE_ENE==="production"
const createRenderer=(bundle, options)=> {
return createBundleRenderer(bundle, Object.assign(options, {
basedir: resolve('./dist'),
runInNewContext: false,
}))
}
let renderer, readyPromise
const templatePath=resolve('./src/index.template.html')
if (isProd) {
const bundle=require('./dist/vue-ssr-server-bundle.json')
const clientManifest=require('./dist/vue-ssr-client-manifest.json')
const template=fs.readFileSync(templatePath, 'utf-8')
renderer=createRenderer(bundle, {
// 推薦
template, // (可選)頁面模板
clientManifest // (可選)客戶端構(gòu)建 manifest
})
} else {
// 開發(fā)模式
readyPromise=require('./config/setup-dev-server')(app, templatePath, (bundle, options)=> {
renderer=createRenderer(bundle, options)
})
}
const render=(req, res)=> {
const context={
title: 'hello ssr with webpack',
meta: `
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
`,
url: req.url
}
renderer.renderToString(context, (err, html)=> {
if (err) {
if (err.code===404) {
res.status(404).end('Page not found')
} else {
res.status(500).end('Internal Server Error')
}
} else {
res.end(html)
}
})
}
// 在服務(wù)器處理函數(shù)中……
app.get('*', isProd ? render : (req, res)=> {
readyPromise.then(()=> render(req, res))
})
app.listen(8080) // 監(jiān)聽的是8080端口
在根目錄新增config文件夾,然后新增四個(gè)配置文件:webpack.base.config,webpack.client.config,webpack.server.config,setup-dev-server(此方法是一個(gè)封裝,為了配置個(gè)熱加載,差點(diǎn)沒搞明白,參考了好多)(官網(wǎng)傳送門: 構(gòu)建配置 )
大部分官網(wǎng)有示例代碼,但是要在基礎(chǔ)上進(jìn)行一些更改
webpack.base.config
// webpack.base.config
const path=require('path')
// 用來處理后綴為.vue的文件
const { VueLoaderPlugin }=require('vue-loader')
const FriendlyErrorsWebpackPlugin=require('friendly-errors-webpack-plugin')
// 定位到根目錄
const resolve=(dir)=> path.join(path.resolve(__dirname, "../"), dir)
// 打包時(shí)會(huì)先清除一下
// const { CleanWebpackPlugin }=require('clean-webpack-plugin')
const isProd=process.env.NODE_ENV==="production"
module.exports={
mode: isProd ? 'production' : 'development',
output: {
path: resolve('dist'),
publicPath: '/dist/',
filename: '[name].[chunk-hash].js'
},
resolve: {
alias: {
'public': resolve('public')
}
},
module: {
noParse: /es6-promise\.js$/,
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhiteSpace: false
}
}
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'url-loader',
options: {
limit: 10000,
name: '[name].[ext]?[hash]'
}
},
{
test: /\.s(a|c)ss?$/,
use: ['vue-style-loader', 'css-loader', 'sass-loader']
}
]
},
performance: {
hints: false
},
plugins:[
new VueLoaderPlugin(),
// 編譯后的友好提示,比如編譯完成或者編譯有錯(cuò)誤
new FriendlyErrorsWebpackPlugin(),
// 打包時(shí)會(huì)先清除一下
// new CleanWebpackPlugin()
]
}
webpack.client.config
// webpack.client.config
const {merge}=require('webpack-merge')
const baseConfig=require('./webpack.base.config.js')
const VueSSRClientPlugin=require('vue-server-renderer/client-plugin')
module.exports=merge(baseConfig, {
entry: {
app: './src/entry-client.js'
},
optimization: {
// 重要信息:這將 webpack 運(yùn)行時(shí)分離到一個(gè)引導(dǎo) chunk 中,
// 以便可以在之后正確注入異步 chunk。
// 這也為你的 應(yīng)用程序/vendor 代碼提供了更好的緩存。
splitChunks: {
name: "manifest",
minChunks: Infinity
}
},
plugins: [
// 此插件在輸出目錄中
// 生成 `vue-ssr-client-manifest.json`。
new VueSSRClientPlugin()
]
})
webpack.server.config
// webpack.server.config
const {merge}=require('webpack-merge')
const nodeExternals=require('webpack-node-externals')
// webpack的基礎(chǔ)配置,比如sass,less預(yù)編譯等
const baseConfig=require('./webpack.base.config.js')
const VueSSRServerPlugin=require('vue-server-renderer/server-plugin')
module.exports=merge(baseConfig, {
// 將 entry 指向應(yīng)用程序的 server entry 文件
entry: './src/entry-server.js',
target: 'node',
// 對(duì) bundle renderer 提供 source map 支持
devtool: 'source-map',
// 此處告知 server bundle 使用 Node 風(fēng)格導(dǎo)出模塊(Node-style exports)
output: {
libraryTarget: 'commonjs2'
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化應(yīng)用程序依賴模塊。可以使服務(wù)器構(gòu)建速度更快,
// 并生成較小的 bundle 文件。
externals: nodeExternals({
// 不要外置化 webpack 需要處理的依賴模塊。
// 你可以在這里添加更多的文件類型。例如,未處理 *.vue 原始文件,
// 你還應(yīng)該將修改 `global`(例如 polyfill)的依賴模塊列入白名單
allowlist: /\.css$/
}),
// 這是將服務(wù)器的整個(gè)輸出
// 構(gòu)建為單個(gè) JSON 文件的插件。
// 默認(rèn)文件名為 `vue-ssr-server-bundle.json`
plugins: [
new VueSSRServerPlugin()
]
})
setup-dev-server:封裝createRenderer方法
const webpack=require('webpack')
const fs=require('fs')
const path=require('path')
const chokidar=require('chokidar')
const middleware=require("webpack-dev-middleware")
const HMR=require("webpack-hot-middleware")
const MFS=require('memory-fs')
const clientConfig=require('./webpack.client.config')
const serverConfig=require('./webpack.server.config')
const readFile=(fs, file)=> {
try {
return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf8')
} catch (error) {
}
}
const setupServer=(app, templatePath, cb)=> {
let bundle
let clientManifest
let template
let ready
const readyPromise=new Promise(r=> ready=r)
template=fs.readFileSync(templatePath, 'utf8')
const update=()=> {
if (bundle && clientManifest) {
// 通知 server 進(jìn)行渲染
// 執(zhí)行 createRenderer -> RenderToString
ready()
cb(bundle, {
template,
clientManifest
})
}
}
// webpack -> entry-server -> bundle
const mfs=new MFS();
const serverCompiler=webpack(serverConfig);
serverCompiler.outputFileSystem=mfs;
serverCompiler.watch({}, (err, stats)=> {
if (err) throw err
// 之后讀取輸出:
stats=stats.toJson()
stats.errors.forEach(err=> console.error(err))
stats.warnings.forEach(err=> console.warn(err))
if (stats.errors.length) return
bundle=JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
update()
});
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin()
)
clientConfig.entry.app=['webpack-hot-middleware/client', clientConfig.entry.app]
clientConfig.output.filename='[name].js'
const clientCompiler=webpack(clientConfig);
const devMiddleware=middleware(clientCompiler, {
noInfo: true, publicPath: clientConfig.output.publicPath, logLevel: 'silent'
})
app.use(devMiddleware);
app.use(HMR(clientCompiler));
clientCompiler.hooks.done.tap('clientsBuild', stats=> {
stats=stats.toJson()
stats.errors.forEach(err=> console.error(err))
stats.warnings.forEach(err=> console.warn(err))
if (stats.errors.length) return
clientManifest=JSON.parse(readFile(
devMiddleware.fileSystem,
'vue-ssr-client-manifest.json'
))
update()
})
// fs -> templatePath -> template
chokidar.watch(templatePath).on('change', ()=> {
template=fs.readFileSync(templatePath, 'utf8')
console.log('template is updated');
update()
})
return readyPromise
}
module.exports=setupServer
在src目錄下,新增index.template.html文件,將官網(wǎng)中的例子(地址:使用一個(gè)頁面模板 )復(fù)制,并進(jìn)行一些更改
<html>
<head>
<!-- 使用雙花括號(hào)(double-mustache)進(jìn)行 HTML 轉(zhuǎn)義插值(HTML-escaped interpolation) -->
<title>{{ title }}</title>
<!-- 使用三花括號(hào)(triple-mustache)進(jìn)行 HTML 不轉(zhuǎn)義插值(non-HTML-escaped interpolation) -->
{{{ meta }}}
</head>
<body>
<!--這個(gè)是告訴我們?cè)谀睦锊迦胝牡膬?nèi)容-->
<!--vue-ssr-outlet-->
</body>
</html>
這里介紹一下一個(gè)很重要的東西asyncData 預(yù)取數(shù)據(jù),預(yù)取數(shù)據(jù)是在vue掛載前,所以下文這里用了上下文來獲取store而不是this
asyncData: ({ store })=> { return store.dispatch('getDataAction') },
在src下創(chuàng)建api文件夾,并在下面創(chuàng)建data.js文件
// data.js
const getData=()=> new Promise((resolve)=> {
setTimeout(()=> {
resolve([
{
id: 1,
item: '測試1'
},
{
id: 2,
item: '測試2'
},
])
}, 1000)
})
export {
getData
}
在src下創(chuàng)建store文件夾,并在下面創(chuàng)建index.js文件
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import { getData } from '../api/data'
export function createStore () {
return new Vuex.Store({
state: {
lists: []
},
actions: {
getDataAction ({ commit }) {
return getData().then((res)=> {
commit('setData', res)
})
}
},
mutations: {
setData (state, data) {
state.lists=data
}
}
})
}
Hello.vue
<template>
<div>
這里是測試頁面一
<p>{{item}}</p>
<router-link to="/hello1">鏈接到測試頁面二</router-link>
</div>
</template>
<script>
export default {
asyncData: ({ store })=> {
return store.dispatch('getDataAction')
},
computed: {
item () {
return this.$store.state.lists
}
}
}
</script>
<style lang="scss" scoped>
</style>
Hello1.vue
<template>
<div>這里是測試頁面二{{item}}</div>
</template>
<script>
export default {
asyncData: ({ store })=> {
return store.dispatch('getDataAction')
},
computed: {
item () {
return this.$store.state.lists
}
}
}
</script>
<style lang="scss" scoped>
</style>
router.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export function createRouter () {
return new Router({
mode: 'history',
routes: [
{
path: '/hello',
component: ()=> import('./components/Hello.vue')
},
{
path: '/hello1',
component: ()=> import('./components/Hello1.vue')
},
]
})
}
app.vue
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
}
</script>
<style lang="scss" scoped>
</style>
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
]
]
}
"dev": "nodemon server.js",
"build": "rimraf dist && npm run build:client && npm run build:server",
"build:client": "webpack --config config/webpack.client.config.js",
"build:server": "webpack --config config/webpack.server.config.js"
大搞告成,執(zhí)行一下dev命令,可以通過訪問localhost:8080端口看到頁面,記得帶上路由哦~
執(zhí)行build命令可看到,最后dist文件下共有三個(gè)文件:main.[chunk-hash].js,vue-ssr-client-manifest.json,vue-ssr-server-bundle.json
附上文件整體目錄結(jié)構(gòu)
??
git 示例demo地址
一對(duì)比,這個(gè)就顯得絲滑多了~ 官網(wǎng)地址: nuxt.js
先對(duì)比一下兩種方案的差別
1.vue初始化雖然有cli,但是nuxt.js的cli更加完備
2.nuxt有更合理的工程化目錄,vue過于簡潔,比如一些component,api文件夾都是要手動(dòng)創(chuàng)建的
3.路由配置:傳統(tǒng)應(yīng)用需要自己來配置,nuxt.js自動(dòng)生成
4.沒有統(tǒng)一配置,需手動(dòng)創(chuàng)建。nuxt.js會(huì)生成nuxt.config.js
5.傳統(tǒng)不易與管理底層框架邏輯(nuxt支持中間件管理,雖然我還沒探索過這里)
顯而易見這個(gè)上手就快多了,也不需要安裝一大堆依賴,如果用了sass需要安裝sass和sass-loader,反正我是用了
npm init nuxt-app <project-name>
nuxt是通過pages頁面形成動(dòng)態(tài)的路由,不用手動(dòng)配置路由。比如在pages下面新增了個(gè)文件about.vue,那么這個(gè)頁面對(duì)應(yīng)的路由就是/about
其實(shí)這個(gè)時(shí)候運(yùn)行npm run dev 就可以看到簡單的頁面了
這里介紹一個(gè)插件,可以快速創(chuàng)建一個(gè)服務(wù)
npm i json-server
安裝完后,在根目錄新增db.json文件,模擬幾個(gè)接口
{
"post": [{"id": 1, "title": "json-server", "author": "jx"}],
"comments": [{"id": 1, "body": "some comment", "postId": 1}],
"profile": {"name": "typicode"}
}
運(yùn)行命令json-server --watch db.json --port=8000(不加會(huì)端口沖突),就可以看到
??
因?yàn)槭莋et請(qǐng)求,可以直接點(diǎn)擊訪問可以看到mock的數(shù)據(jù)已經(jīng)返回了
??
先配置一下axios,推薦使用nuxt.js封裝的axios:"@nuxtjs/axios": "^5.13.6",然后再在nuxt.config.js文件中modules下面配置一下就可以使用了
modules: [ '@nuxtjs/axios'],
隨便找個(gè)接口調(diào)用一下
<template>
<div>
<div>
這里是測試頁面一
</div>
接口返回?cái)?shù)據(jù):{{posts}}
</div>
</template>
<script>
export default {
name: 'IndexPage',
async asyncData({$axios}){
const result=await $axios.get('http://localhost:8000/post')
return{
posts: result.data
}
}
}
</script>
刷新下頁面就可以看到效果了,這里注意$axios有兩個(gè)get方法,一個(gè)$axios.get還會(huì)返回頭部等信息,另一個(gè)$axios.$get只返回結(jié)果
從頁面篇幅上應(yīng)該也能看到哪個(gè)容易上手了,nuxt相對(duì)于插件來說限定了文件夾的結(jié)構(gòu),并通過此預(yù)定了一些功能,更好上手。預(yù)設(shè)了利用vue.js開發(fā)服務(wù)端渲染所需要的各種配置,并且提供了提供了靜態(tài)站點(diǎn),異步數(shù)據(jù)加載,中間件支持,布局支持等
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。