对于比较传统的后端渲染项目,前端一直处于比较尴尬的位置,很多时候前端就是写一些样式一些动效.然后直接把写好的html文件扔给后端(jsp之类的木板)这就让很多后端工程师工作繁重了不少.但是由于很多后端的js不是那么的熟练.导致了两个问题:1. 事件绑定直接在dom上进行.后期改动代码变得很复杂. 2. 很多后端逻辑直接写到了模板里. 导致前后端耦合极深,对于后端的重构也不是什么好事.

现在前端的趋势是大前端,然而大部分前端还处于几年前的状态,仍然是后端主导.但是既然拿钱干活了我们就要把事情做好.也为了自己的话语权(方便自己嘛).啰嗦了这么多,下面就是正文部分.

做什么,实现什么样的效果

对于大部分前端来说, 我们要怎么才方便呢:热加载!主流前端框架通过webpack进行一定配置都可以实现热加载.那么后端渲染的项目可不可以呢,当然也是可以的.我们想想前段时间很火的browser-sync,这货无论是作为一个命令行还是gulp的插件,用起来都及其爽.找了一下,我发现webpack同样有browse-sync的插件:browser-sync-webpack-plugin.

代码的热更新,这个1同样没有问题,我们都知道webpack-dev-server,这货就是实现这个功能的,webpack也有相关配置

除了热加载还有什么呢,es6?sass?postcss?ts? 都是可以的,因为都是代码编译的,所以这部分跟普通框架的配置并没有什么不一样.但是需要注意的就是如果你使用了jquery插件又使用了babel-preset-es2015 可能会发生错误我看了一下是因为preset默认use strict,但是某些插件有些冲突(会报变量undefined),但是我又懒得改.所以没加上.bablerc. 使用babel-preset-es2015-without-strict同样没用.会报另外的错误,当然这是遗留问题,如果你不使用插件(也不推荐使用)就没啥问题了.

总结一下就是两件事:

  1. 热更新减少ctrl + r的使用率,如果有两个屏幕的话那就爽了(我并没有)

  2. 追新哈哈,其实也是为了开发方便,jquery一时是仍不掉了,但是我可以优化开发过程

为什么要这么做

其实并没有什么理由,你说开发方便吧,但是配置麻烦啊.各有得失吧,这样做是把工作量往自己身上揽,毕竟如果类似那些外包的做法.前端会省很多力气(毕竟很多js都是后端来写).对于活动页面就没必要这么折腾了.毕竟只用那么几次,不需要考虑后续重构之类的问题.

怎么实现

首先考虑如果是vue的单页应用我们改怎么实现打包后的资源生成与插入.如果是前端渲染,不管是单页还是多页我们都可以通过html-webpack-plugin插件通过模板文件生成需要的html资源,但是对于后端渲染的页面就不能这么做了,其实非要做也是可以的,但是可能每个页面都需要引入不同的资源文件,而且后端模板分的比较开,比如nunjucks直接引入文件还是不行的,必须引入到block中,这样一来就复杂了.也没太必要.

还有一个更好的做法,webpack打包的时候生成资源文件索引manifest,然后后端通过索引在需要的地方插入资源文件.这里同样的有个插件webpack-manifest-plugin,这个是在输出目录里生成资源文件列表,如果配合webpac-dev-server,可能并存在输出目录,文件都在内存中,但是应该可以配置writeToFileEmit: true生成实体文件让后端读取.为什么事应该呢,因为我没用这个插件,插件是后来才发现的,之前是这么写的:

plugin: [
  ...
          function () {
            this.plugin('done', function(stats){
                require('fs').writeFileSync(path.join(config.mapPath, "map.json"), JSON.stringify(stats.toJson().assetsByChunkName, null, 4));
            })
        }
]

其实这也是简单的webpack插件的写法,再深层次的我也不是很清楚了.

生成资源文件索引以后,后端可以配置cdn的路径(一般来说应该是可配置的吧),由于webpack-dev-server是另外起一个服务器,所以就当是cdn了,当然如果你后端是nodejs可以使用webpack-dev-middleware代替webpack-dev-server 这个配置就不多说了.配置webpack中的devServer:

devServer: {
        contentBase: path.join(__dirname, 'dist'),
        publicPath: '/assets',
        host: "0.0.0.0",
        compress: true,
        hot: true
    }

具体路径可以根据自己的需要来修改,这么一来我们的webpack就可以进行动态编译了,不需要改动一点点就全部编译.一般来说文件都会加上hash,但是没有影响因为索引也是有hash的,后端直接引用就可以了.

下面就进行热重载的配置:

plugins: [
        new BrowserSyncPlugin({
            // browse to http://localhost:3000/ during development,
            // ./public directory is being served
            host: 'localhost',
            port: 3000,
            proxy: 'localhost:8765'
        })
]

跟一般的前端重载不一样,这里我们需要设置代理,也就是proxy,因为页面是后端渲染的,所以访问链接也是后端提供的.代理好了后配合webpack-dev-server,一旦编译好了浏览器就会自动刷新.但是需要注意,修改后端模板文件浏览器不会刷新需要手动,毕竟是后端渲染的.

这样一来我们上面的两个需要算是基本实现了,还有其他的基础功能这里就不啰嗦了,什么配置es6,sass之类的,都是通过一些loader进行配置,对了,这里使用的是webpack2.

最后放上完整的配置:

const webpack = require('webpack')
const CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
const CleanPlugin = require('clean-webpack-plugin');
const BrowserSyncPlugin = require('browser-sync-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const path = require('path')
const glob = require('glob')
const config = require('./config.build')
// isProduct 判断是否是生产环境
const isProduct = (process.env.NODE_ENV === 'production')
console.log(process.env.NODE_ENV)
const entryPath = path.resolve(config.projectPath, config.entryPathName)
const baseURL = !isProduct ? 'http://localhost:8080/assets/' : '/site/static/'
const commonModulePath = path.resolve('./assets/src/modules')
const commonPluginPath = path.resolve('./assets/src/plugins')
const getEntry = entries => {
    const entry = {}
    const srcDirName = entries + '/**/*.js'
    glob.sync(srcDirName).forEach(function (filepath) {
        const name = filepath.slice(filepath.lastIndexOf(config.entryPathName) + config.entryPathName.length + 1, -3);
        entry[name] = filepath;
    })
    return entry
}
module.exports = {
    entry: getEntry(entryPath),
    output: {
        path: config.buildPath,
        filename: 'js/[name].[chunkhash:6].js',
        publicPath: baseURL
    },
    module: {
        rules: [
            {
                test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'
            },
            {
                test: /\.css$/,
                loader: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: 'css-loader?sourceMap&minimize!postcss-loader?sourceMap'
                })
            },
            {
                test: /\.scss$/,
                loader: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: 'css-loader?sourceMap&minimize!sass-loader?sourceMap'
                })
            },
            {
                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                loader: 'url-loader',
                query: {
                    limit: 10000,
                    name: 'images/[name].[hash:7].[ext]'
                }
            },
            {
                test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
                loader: 'url-loader',
                query: {
                    limit: 10000,
                    name: 'fonts/[name].[hash:7].[ext]'
                }
            }
        ]
    },
    resolve: {
        modules: [
            path.join(config.projectPath, './src/js'),
            'node_modules'
        ],
        extensions: ['.js', '.json', '.scss'],
        alias: {
            commonModule: commonModulePath,
            css: path.resolve(__dirname, 'plugins/Site/webroot/css'),
            commonPlugin: commonPluginPath,
            module: path.resolve(config.modulePath),
            plugin: path.resolve(config.pluginPath),
            layer: 'commonPlugin/layer/layer.js',
            lazyload: 'commonPlugin/jquery.lazyload.min.js',
            cookie: 'commonPlugin/js.cookie.js',
            dmuploader:  'commonPlugin/uploader/src/dmuploader.min.js',
            tmpl:  'commonPlugin/wu.tmpl.js/wu.tmpl.js',
            prettySocial: 'commonPlugin/prettySocial/jquery.prettySocial.js',
            "jquery.rating": 'commonPlugin/jquery-star-rating/src/rating.js',
            "jquery.rating.css": 'commonPlugin/jquery-star-rating/src/rating.css',
            "jquery.validate": "jquery-validation",
        }
    },
    externals: {
        'jquery': 'window.$',
        'lodash': 'window._'
    },
    devtool: 'source-map',
    plugins: [
        new BrowserSyncPlugin({
            // browse to http://localhost:3000/ during development,
            // ./public directory is being served
            host: 'localhost',
            port: 3000,
            proxy: 'localhost:8765'
        }),
        new CleanPlugin(['*'], {
            root: path.resolve(config.buildPath)
        }),
        new ExtractTextPlugin({ filename: "css/[name]-[chunkhash:6].css", allChunks: true }),
        new webpack.ProvidePlugin({
            $: "jquery",
            jQuery: "jquery",
            "window.jQuery": "jquery",
            "_": "lodash"
        }),
        new CommonsChunkPlugin({
            name: "common",
            filename: 'common-[chunkhash:6].js',
            minChunks: 3
        }),
        // new webpack.optimize.UglifyJsPlugin({
        //     minimize: true,
        //     compress: {
        //         warnings: false
        //     },
        //     comments: false
        // }),
        //生成编译之后的文件映射
        function () {
            this.plugin('done', function(stats){
                require('fs').writeFileSync(path.join(config.mapPath, "map.json"), JSON.stringify(stats.toJson().assetsByChunkName, null, 4));
            })
        }
    ],
    devServer: {
        contentBase: path.join(__dirname, 'dist'),
        publicPath: '/assets',
        host: "0.0.0.0",
        compress: true,
        hot: true
    }
}

原文地址: webpack传统后端渲染的项目前端配置
惯例放上二维码:
image


xiadd
2.6k 声望88 粉丝