常规的webpack配置,需要注意到在dev环境中contentBase 不能随意添加,与pulicepat相关
webpack.base.js基本设置文件,基本的常规设置
const path = require('path')
module.exports = {
output: {
path: path.join(__dirname, '../dist'),
publicPath: '/public/'
},
resolve: {
extensions: ['.js', '.jsx']
},
module: {
rules: [
{
enforce: 'pre',
test: /\.(js|jsx)$/,
use: ['eslint-loader'],
exclude: [path.resolve(__dirname, '../node_modules')]
},
{
test: /\.jsx$/,
loader: 'babel-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: [path.join(__dirname, '../node_modules')]
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|svg|jpg|gif)$/,
use: ['file-loader']
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: ['file-loader']
},
{
test: /\.(csv|tsv)$/,
use: ['csv-loader']
},
{
test: /\.xml$/,
use: ['xml-loader']
}
]
}
}
const path = require('path')
const HtmlPlugin = require('html-webpack-plugin')
const Webpack = require('webpack')
const webpackMerge = require('webpack-merge')
const baseConfig = require('./webpack.base')
const isDev = process.env.NODE_ENV === 'development'
const config = webpackMerge(baseConfig, {
entry: {
app: path.join(__dirname, '../client/app.js')
},
output: {
filename: '[name].[hash].js',
path: path.join(__dirname, '../dist'),
publicPath: '/public/'
// 前缀,区分api接口,和静态资源,加入cdn,注意和
},
plugins: [
new HtmlPlugin({
template: path.join(__dirname, '../client/template.html')
}),
new HtmlPlugin({
template: '!!ejs-compiled-loader!' + path.join(__dirname, '../client/template.ejs'),
filename: 'server.ejs'
})
]
})
if (isDev) {
config.devServer = {
// 可以通过任何方式,最合适
host: '0.0.0.0',
port: '8888',
hot: true,
compress: true,
overlay: {
errors: true
},
publicPath: '/public/',
historyApiFallback: {
index: '/public/index.html'
},
proxy: {
'/api': 'http://localhost:3000'
}
}
config.plugins.push(
new Webpack.NamedModulesPlugin(),
new Webpack.HotModuleReplacementPlugin()
)
config.entry = {
app: ['react-hot-loader/patch', path.join(__dirname, '../client/app.js')]
}
}
module.exports = config
使用cross-env来跨平台传递参数dev:client": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.client.js
,到此步才引入webpack-dev-sever,即开发服务器配置,在webpack配置文件中通过process中的参数进行判断,webpack3中dev的配置简化了很多(注意需要全局安装webpack-dev-sever).此时开发服务器读入的是build后的文件, 要注意dist目录,在开发环境的时候,webpack默认会读dist目录,可能会因为hash,到不到默认的版本出现文件404的错误
publicPath 需要和基础配置相同。
historyApiFallback 配置了很多关系,404的资源会返回到index.html
const path = require('path')
const webpackMerge = require('webpack-merge')
const baseConfig = require('./webpack.base')
// 服务端打包是从客户端读入
module.exports = webpackMerge(baseConfig, {
target: 'node',
entry: {
app: path.join(__dirname, '../client/server.entry.js')
},
output: {
// 服务端是读webpack的输出目录
filename: 'server.entry.js',
// 前缀,区分api接口,和静态资源,加入cdn
// 使用最新的node.js模块方案,适用于
libraryTarget: 'commonjs2'
}
})
配置到这步的时候,在服务端读取dist(经过webpack打包过后的build文件和模板)。先读模板,然后使用ReactDOMServer.renderToString(app)得到字符串模板。再返回即可
保存react组件状态,避免程序刷新然后到初始状态,通过json的更新来改变,只有在开发状态需要开启热更新,真是哔了狗了,有个代码热更新一直实现不了,我重新来了,最后发现把webpack-dev-server 我装成webpack-dev了
webpack-dev-server开始启替换很简单,再plugins中使用 new webpack.NamedModulesPlugin(), new webpack.HotModuleReplacementPlugin()
即可但是react组件要启动热替换需要安装React Hot Loader,以及在dev环境中的
// 热更新
if (module.hot) {
module.hot.accept('./app.js', () => { render(App) })
}
需要import { AppContainer } from 'react-hot-loader'
需要写模块判断
需要在webpack的entry入口文件中引入
config.entry = {
app: ['react-hot-loader/patch', path.join(__dirname, '../client/app.js')]
}
注意publicpath的路径,会引起jsonUpate的路径不对
node.js 开发服务端环境的配置
在开发预览的时候,需要对开发服务器进行预览,免去必须build一次,然后再开启服务端渲染的问题.
一种及其hack的写发,首先在node.js服务器中导入webpack配置,然后使用webpack运行配置文件,webpack watch客户端开发环境的配置,然后使用webpack
总体的思路是使用mfs读取webpack-dev-server在内存里打包的文件,读取成字符串,然后使用node的module模块进行编译,然后暴露再render
const axios = require('axios')
const webpack = require('webpack')
const path = require('path')
const Memoryfs = require('memory-fs')
const ReactDomServer = require('react-dom/server')
// 代理
const proxy = require('http-proxy-middleware')
/**
* 大致思路是在node.js的环境种读取webpack的编译代码,这个方式过于hack
* 可以参考社区里的实现
*/
const serverConfig = require('../../webpack/webpack.config.server')
// 拿到入口文件
const getTemplate = () => {
return new Promise((resolve, reject) => {
axios.get('http://localhost:8888/public/index.html').then(res => {
resolve(res.data)
}).catch(reject)
})
}
const mfs = new Memoryfs()
// 模块的构造函数
const Module = module.constructor
let serverBundle
// 设置webpack的读写模式
const serverCompiler = webpack(serverConfig)
serverCompiler.outputFileSystem = mfs
// webpack的watch
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
stats = stats.toJson()
stats.errors.forEach(err => console.log(err))
stats.warnings.forEach(warn => console.log(warn))
// 得到路径
const bundlePath = path.join(serverConfig.output.path, serverConfig.output.filename)
// 使用fs读取内存种的代码
const bundle = mfs.readFileSync(bundlePath, 'utf-8')
const m = new Module()
// 必须指定名字,不然报错,及其hack的写法!
m._compile(bundle, 'server-entry.js')
serverBundle = m.exports.default
})
module.exports = function (app) {
// 代理请求到webpacdev上
app.use('/public', proxy({
target: 'http://localhost:8888'
}))
// 所有的请求都返回静态结果,都在内存中
app.get('*', function (req, res) {
getTemplate().then(template => {
const content = ReactDomServer.renderToString(serverBundle)
res.send(template.replace('<!-- app -->', content))
})
})
}
目前的问题问题有 eslint和mobx的问题,已经解决eslint的版本问题
使用者可能从任意路由进入到我们的网站,所以在服务端中必须处理路由跳转,在返回给客户端的时候就指定页面,需要在服务端支持重定向 每个页面会有对应的数据,在服务端渲染的时候已经请求过对应的数据,所以要让客户端知道这些数据,在客户端渲染的时候直接使用,而不是api请求,造成浪费
- 路由跳转 当客户端代码有重定向的时候,服务端要如何处理Redirect? StaticRouter是服务端渲染的静态路由组件,StaticRouter的context是一个js对象,记录了服务端渲染的结果,location表示 当前位置,具体可以是node.js的req.url 在客户端有重定向的时候,staticRouter的context则会被加上一个url的属性
在client中的暴露给服务端的入口文件,默认暴露一个function,返回了一个Provider包裹的组件 JavaScript /*
- @param {stores} 在服务端渲染的时候,需要重新创建? 在mobx中可以使用多个stores,避免重复渲染
- @param {routerContext} 路由信息 StaticRouter接收两个参数,一个是context,
- 静态渲染的时候的信息,假如有重定向的时候,一个是location
- @param {url} 现在请求的url,可以通过node.js的req直接拿到 */ export default (stores, routerContext, url) => ( <Provider {...stores}> ,在node.js通过webpack.watch()的方法
在node.js的渲染逻辑中使用mfs模块读出webpack编译后的代码,使用node.js的原生模块的构造函数,然后compiler运行,得到m.exports即就是暴露的模块拿到入口方法
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
stats = stats.toJson()
stats.errors.forEach(err => console.log(err))
stats.warnings.forEach(warn => console.log(warn))
// 得到路径
const bundlePath = path.join(serverConfig.output.path, serverConfig.output.filename)
// 使用fs读取内存种的代码
const bundle = mfs.readFileSync(bundlePath, 'utf-8')
const m = new Module()
// 必须指定名字,不然报错,及其hack的写法!
m._compile(bundle, 'server-entry.js')
serverBundle = m.exports.default
// 拿到客户端中的方法
createStoreMap = m.exports.createStoreMap
})
如何处理异步数据?
使用react-async-bootstrapper,在asyncBootstrap()中的方法,会先执行初始化代码才会进行其他 .
asyncBootstrap(app).then(() => {
if (routerContext.url) {
// 如果有302
res.status(302).setHeader('Location', routerContext.url)
res.end()
return
}
const state = getStoreState(stores)
const content = ReactDomServer.renderToString(app)
const html = ejs.render(template, {
appString: content,
initialState: serialize(state)
})
res.send(html)
})
使用ejs模板引擎
ejs模板,服务端获取到客户端初始状态的一些
ejs,serialize-javascript(解决在服务端初始状态为 object,object在传送中会被toString)
如何解决客户端和服务端代码不同步的问题 ?
mobx会存在多个store,实现起来逼redux略复杂
const getStoreState = (stores) => {
return Object.keys(stores).reduce((result, storeName) => {
// 数据默认值的情况
result[storeName] = stores[storeName].toJson()
return result
}, {})
}
mobox的Warning的问题, there are multiple mobx instances active. This might lead to unexpected results,mobx的多次实例化
解决方法:externals: Object.keys(require('../package.json').dependencies),将类库中的代码提取出来
const NativeModule = require('module')
const vm = require('vm')
// `(function(exports, require, module, __finename, __dirname){ ...bundle code })`
const getModuleFromString = (bundle, filename) => {
const m = { exports: {} }
// 将js代码包装成// `(function(exports, require, module, __finename, __dirname)
// { ...bundle code })`
const wrapper = NativeModule.wrap(bundle)
// 运行并
const script = new vm.Script(wrapper, {
filename: filename,
displayErrors: true,
})
// 执行执行环境
const result = script.runInThisContext()
// 让M去执行,并拿到
result.call(m.exports, m.exports, require, m)
return m