Skip to content

Commit

Permalink
feat: merge static assets and webpack assets into a single assets.jso…
Browse files Browse the repository at this point in the history
…n, and pass it from server to client via imvc context, add ctrl.getClientAssetPath API to access them
  • Loading branch information
Lucifier129 committed Sep 28, 2023
1 parent 804dea2 commit a2d5f28
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 94 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ node_modules

# production
publish
project_publish
.idea

# misc
Expand Down
68 changes: 68 additions & 0 deletions build/assets-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import fg from 'fast-glob'
import path from 'path'

export const getStaticFiles = async (cwd) => {
const files = await fg([
// match all non-js/ts/jsx/tsx files
`**/!(*.@(js|ts|jsx|tsx))`,
// match all files in lib
`lib/**/*`,
], {
cwd
})
return files
}

/**
* get static assets which are not js/ts/jsx/tsx files in cwd
* will merge into webpack assets.json
* @param cwd
* @returns
*/
export const getStaticAssets = async (cwd) => {
const files = await getStaticFiles(cwd)
const assets = {}

for (const file of files) {
assets[file] = file
}

return assets
}

export function getAssets(stats) {
return Object.keys(stats).reduce((result, assetName) => {
let value = stats[assetName]
result[assetName] = Array.isArray(value) ? value[0] : value
return result
}, {})
}


export function readAssets(config) {
let result
// 生产模式直接用编译好的资源表
let assetsPathList = [
// 在 publish 目录下启动
path.join(config.root, config.static, config.assetsPath),
// 在项目根目录下启动
path.join(config.root, config.publish, config.static, config.assetsPath),
]

while (assetsPathList.length) {
try {
let itemPath = assetsPathList.shift()
if (itemPath) {
result = require(itemPath)
}
} catch (error) {
// ignore error
}
}

if (!result) {
throw new Error('找不到 webpack 资源表 assets.json')
}

return getAssets(result)
}
13 changes: 8 additions & 5 deletions build/createGulpTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,17 @@ const createConfig = options => {
},
publishCopy: {
src: [
root + `/!(node_modules|${options.publish}|buildportal-script)/**/*`,
root + `/!(node_modules|${options.publish}|buildportal-script)`
`!${path.join(root, options.publish)}`,
root + `/!(node_modules|buildportal-script)/**/*`,
root + `/!(node_modules|buildportal-script)`
],
dest: publish
},
publishBabel: {
src: [
root +
`/!(node_modules|${options.publish}|buildportal-script)/**/*.@(js|ts|jsx|tsx)`,
publish + '/*.@(js|ts|jsx|tsx)'
`!${path.join(root, options.publish)}`,
root + `/!(node_modules|buildportal-script)/**/*.@(js|ts|jsx|tsx)`,
root + '/*.@(js|ts|jsx|tsx)'
],
dest: publish
}
Expand Down Expand Up @@ -93,6 +94,8 @@ const removeBabelRuntimePlugin = (babelConfig) => {
module.exports = function createGulpTask(options) {
let config = Object.assign(createConfig(options))

console.log('gulp config:', config)

let minifyCSS = () => {
if (!config.css) {
return
Expand Down
28 changes: 22 additions & 6 deletions build/createWebpackConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ const PnpWebpackPlugin = require('pnp-webpack-plugin')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const resolve = require('resolve')
const { checkFilename } = require('./compileNodeModules')
const { getStaticAssets } = require('./assets-helper')

module.exports = function createWebpackConfig(options, isServer = false) {
module.exports = async function createWebpackConfig(options, isServer = false) {
let result = {}
let config = Object.assign({}, options)
let root = path.join(config.root, config.src)
Expand Down Expand Up @@ -56,15 +57,30 @@ module.exports = function createWebpackConfig(options, isServer = false) {

let output = Object.assign(defaultOutput, config.output)

let staticAssets = await getStaticAssets(root)

let plugins = [
!isServer && new ManifestPlugin({
fileName: config.assetsPath,
map: file => {
// 删除 .js 后缀,方便直接使用 obj.name 来访问
if (/\.js$/.test(file.name)) {
file.name = file.name.slice(0, -3)
generate: (_seed, files, _entries) => {
const assets = { ...staticAssets }

for (const file of files) {
if (!file.name) {
continue
}

assets[file.name] = file.path

// 生成一个不带后缀的文件名
// assets.vendor 可以访问到 vendor.js
// assets.index 可以访问到 index.js
if (file.name && /\.js$/.test(file.name)) {
assets[file.name.slice(0, -3)] = file.path
}
}
return file

return assets
}
}),
// Moment.js is an extremely popular library that bundles large locale files
Expand Down
8 changes: 4 additions & 4 deletions build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ function delPublish(folder) {
return del(folder)
}

function startWebpackForClient(config) {
let webpackConfig = createWebpackConfig(config, false)
async function startWebpackForClient(config) {
let webpackConfig = await createWebpackConfig(config, false)
return new Promise(function (resolve, reject) {
webpack(webpackConfig, function (error, stats) {
if (error) {
Expand All @@ -61,8 +61,8 @@ function startWebpackForClient(config) {
})
}

function startWebpackForServer(config) {
let webpackConfig = createWebpackConfig(config, true)
async function startWebpackForServer(config) {
let webpackConfig = await createWebpackConfig(config, true)
return new Promise(function (resolve, reject) {
webpack(webpackConfig, function (error, stats) {
if (error) {
Expand Down
8 changes: 4 additions & 4 deletions build/setup-dev-env.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ const MFS = require('memory-fs')
const notifier = require('node-notifier')
const createWebpackConfig = require('./createWebpackConfig')

exports.setupClient = function setupClient(config) {
exports.setupClient = async function setupClient(config) {
let startTime = Date.now()
console.log('client webpack is starting...')
let clientConfig = createWebpackConfig(config)
let clientConfig = await createWebpackConfig(config)
let compiler = webpack(clientConfig)
return new Promise(resolve => {
let isResolved = false
Expand Down Expand Up @@ -38,11 +38,11 @@ exports.setupClient = function setupClient(config) {
})
}

exports.setupServer = function setupServer(config, options) {
exports.setupServer = async function setupServer(config, options) {
let startTime = Date.now()
console.log('server webpack is starting...')

let serverConfig = createWebpackConfig(config, true)
let serverConfig = await createWebpackConfig(config, true)

if (!serverConfig.output?.path || !serverConfig.output.filename) {
throw new Error('serverConfig.output.path and serverConfig.output.filename must be specified')
Expand Down
44 changes: 41 additions & 3 deletions controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export default class Controller {
if (context.isServer && finalOptions.credentials === 'include') {
finalOptions.headers['Cookie'] = context.req.headers.cookie || ''
}

// 支持使用方手动传入自定义fetch方法
let rawFetch = context.isServer ? global.fetch : window.fetch
let finalFetch = typeof options.fetch === 'function' ? options.fetch : rawFetch
Expand All @@ -226,7 +226,7 @@ export default class Controller {
if (typeof options.timeout === 'number') {
let { timeoutErrorFormatter } = options
let timeoutErrorMsg = typeof timeoutErrorFormatter === 'function' ?
timeoutErrorFormatter({ url, options: finalOptions }) : timeoutErrorFormatter
timeoutErrorFormatter({ url, options: finalOptions }) : timeoutErrorFormatter
fetchData = _.timeoutReject(fetchData, options.timeout, timeoutErrorMsg)
}

Expand Down Expand Up @@ -266,6 +266,41 @@ export default class Controller {
}
return this.fetch(url, options)
}
/**
* 基于 webpack 构建的 assets.json 获取客户端的静态资源路径
*/
getClientAssetPath(assetPath) {
if (this.context.isServer) {
return assetPath
}

let [pathname, search] = assetPath.split('?')

let assets = this.context.assets ?? {}
let realAssetPath = assets[pathname]

if (realAssetPath) {
if (!realAssetPath.startsWith('/')) {
realAssetPath = '/' + realAssetPath
}

if (search) {
// 保留原有的 querystring
return `${realAssetPath}?${search}`
}

return realAssetPath
}

/**
* 如果未匹配到,尝试去掉前缀再试一次
*/
if (assetPath.startsWith('/')) {
return this.getClientAssetPath(assetPath.slice(1))
}

return assetPath
}
/**
* 预加载 css 样式等资源
*/
Expand All @@ -282,7 +317,10 @@ export default class Controller {
if (context.preload[name]) {
return
}
let url = preload[name]
/**
* 获取资源的真实路径(可能经过 bundler 处理为 hash 值)
*/
let url = this.getClientAssetPath(preload[name])

if (!_.isAbsoluteUrl(url)) {
if (context.isServer) {
Expand Down
51 changes: 14 additions & 37 deletions entry/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import helmet from 'helmet'
import ReactViews from 'express-react-views'
import shareRoot from '../middleware/shareRoot'
import configBabel from '../config/babel'
import { getAssets, getStaticAssets, readAssets } from '../build/assets-helper'

export default async function createExpressApp(config) {
const app = express()
Expand Down Expand Up @@ -108,10 +109,18 @@ export default async function createExpressApp(config) {
express.static(path.join(config.root, config.src))
)

const staticAssets = await getStaticAssets(path.join(config.root, config.src))

// 开发模式用 webpack-dev-middleware 获取 assets
app.use((req, res, next) => {
app.use(async (req, res, next) => {
const assetsPath = path.join(config.root, config.publish, config.static, config.assetsPath)
const assetsJson = JSON.parse(res.locals.fs.readFileSync(assetsPath, 'utf-8'))

res.locals.assets = getAssets(
res.locals.webpackStats.toJson().assetsByChunkName
{
...staticAssets,
...assetsJson
}
)
next()
})
Expand Down Expand Up @@ -167,7 +176,8 @@ export default async function createExpressApp(config) {
publicPath,
restapi: config.restapi,
...config.context,
preload: {}
preload: {},
assets: res.locals.assets
}

res.locals.appSettings = {
Expand All @@ -181,37 +191,4 @@ export default async function createExpressApp(config) {
})

return app
}

function getAssets(stats) {
return Object.keys(stats).reduce((result, assetName) => {
let value = stats[assetName]
result[assetName] = Array.isArray(value) ? value[0] : value
return result
}, {})
}

function readAssets(config) {
let result
// 生产模式直接用编译好的资源表
let assetsPathList = [
// 在 publish 目录下启动
path.join(config.root, config.static, config.assetsPath),
// 在项目根目录下启动
path.join(config.root, config.publish, config.static, config.assetsPath)
]

while (assetsPathList.length) {
try {
result = require(assetsPathList.shift())
} catch (error) {
// ignore error
}
}

if (!result) {
throw new Error('找不到 webpack 资源表 assets.json')
}

return getAssets(result)
}
}
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-imvc",
"version": "2.10.5",
"version": "2.10.6",
"description": "An Isomorphic MVC Framework",
"main": "./index",
"bin": {
Expand Down Expand Up @@ -59,6 +59,7 @@
"expect": "^1.20.2",
"express": "^4.14.0",
"express-react-views": "^0.10.5",
"fast-glob": "^3.3.1",
"fancy-log": "^1.3.3",
"fork-ts-checker-webpack-plugin": "^6.2.0",
"gulp": "^4.0.0",
Expand Down Expand Up @@ -95,16 +96,16 @@
"yargs": "^8.0.2"
},
"peerDependencies": {
"react": "^16.8.0",
"react-dom": "^16.8.0",
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0",
"@babel/runtime": "^7.22.10"
},
"devDependencies": {
"@playwright/test": "^1.20.0",
"nyc": "^15.1.0",
"puppeteer": "5.2.1",
"react": "^16.8.0",
"react-dom": "^16.8.0",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"rimraf": "^2.6.1",
"typescript": "^4.2.3"
}
Expand Down
Loading

0 comments on commit a2d5f28

Please sign in to comment.