elpis之前端工程化

elpis之前端工程化

上文回顾

上篇文章当中,我们完成了elpis-core 的开发以及应用,接下来我们将继续进行elpis 的后续开发。本文需要完成的是前端工程化相关的内容。

前端工程化的概念

前端工程化 是个很大的概念,他是我们前端开发的一个总体的思想,包含模块化、包管理器等东西,但单从框架的角度出发(vue/react),前端工程化代表的是类似于elpis-core 的一个解析引擎,他将浏览器除JavaScript、CSS、HTML以外的文件通过类似于elpis-code 中的loader转化为浏览器认识的JavaScript文件。

如图所示,当我们通过框架写完业务代码之后,解析引擎通过一系列操作最终生成浏览器认识的html、css、js以及各种类型的图片文件。最终将产物文件交予服务器,服务器最终在浏览器输出用户可浏览的前端网页。

elpis的前端解析引擎

为什么是webpack

  1. webpack的官方文档比较清晰明了
  2. webpack的社区生态非常完善
  3. 本人对于webpack比较了解
为什么采取多入口

elpis的初衷是为了节省80%的重复的工作,为剩余的20%提供自定义。因此在某个场景之下(比如电商),每个不同的系统都有其重复的部分,为了能够将其中重复的部分抽取出来而保留差异,我们需要使用多入口来实现。

基础webpack配置

js 复制代码
/**
 * 获取app/pages下所有文件
 */
const glob = require('glob')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const pageEntries = {}
const htmlWebpackPlugins = []
const entryList = path.resolve(process.cwd(), './app/pages/**/entry.*.js')

glob.sync(entryList).forEach(file => {
    const filename = path.basename(file, '.js')
    pageEntries[filename] = file
    htmlWebpackPlugins.push(
        new HtmlWebpackPlugin({
            filename: path.resolve(process.cwd(), './app/public/dist/', `${filename}.tpl`),
            template: path.resolve(process.cwd(), './app/view/entry.tpl'),
            chunks: [filename]
        })
    )
})

module.exports = {
    pageEntries,
    htmlWebpackPlugins
}
js 复制代码
/**
 * webpack 基础配置
 */
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const webpack = require('webpack')
const { pageEntries, htmlWebpackPlugins } = require('../utils')

module.exports = {
    // 入口配置
    entry: pageEntries,
    // 模块解析配置(决定了要加载解析哪些模块,以及用什么方式解析)
    module: {
        rules: [
            {
                test: /\.js$/,
                include: [path.resolve(process.cwd(), './app/pages')],
                use: {
                    loader: 'babel-loader'
                }
            },
            {
                test: /\.vue$/,
                use: {
                    loader: 'vue-loader'
                }
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.less$/,
                use: ['style-loader', 'css-loader', 'less-loader']
            },
            {
                test: /\.(png|jpe?g|gif)(\?.+)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        limit: 300,
                        esModule: false
                    }
                }
            },
            {
                test: /\.(eot|svg|woff|woff2|ttf)(\?\S*)?$/,
                loader: 'file-loader'
            }
        ]
    },
    // 产物输出路径,不同环境各自配置
    output: {},
    // 配置模块解析具体行为(定义webpack在打包时,如何找到并解析具体模块的路径)
    resolve: {
        extensions: ['.js', '.vue', '.less', '.css'],
        alias: {
            $pages: path.resolve(process.cwd(), './app/pages'),
            $common: path.resolve(process.cwd(), './app/pages/common'),
            $widgets: path.resolve(process.cwd(), './app/pages/widgets'),
            $store: path.resolve(process.cwd(), './app/pages/store')
        }
    },
    // 配置webpack插件
    plugins: [
        // 处理.vue文件
        // 将定义的其他规则复制应用到vue文件中的每一个部分当中(template、script、style)
        new VueLoaderPlugin(),
        // 把第三方库暴露到window context
        new webpack.ProvidePlugin({
            Vue: 'vue',
            axios: 'axios',
            _: 'lodash'
        }),
        // 定义全局变量(常量)
        new webpack.DefinePlugin({
            __VUE_OPTIONS_API__: 'true', // 支持vue解析options api
            __VUE_PROD_DEVTOOLS__: 'false', // 禁用Vue调试工具
            __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false' // 禁用生产环境展示水合信息
        }),
        // 构造最终渲染的模板
        ...htmlWebpackPlugins
    ]
}

打包优化

js 复制代码
// 配置打包输出优化 (代码分割、模块合并、缓存、TreeShaking、压缩)
optimization: {
    /**
     * 把js文件打包成3中类型
     * 1. vendor: 第三方lib库,基本不会改变,除非依赖版本升级
     * 2. common: 业务组件代码的公共部分抽取出来,改动较少
     * 3. entry.{page}: 不用页面entry里的业务组件代码的差异部分,会经常改动
     * 目的: 把改动和引用频率不一样的js文件区分出来,已达到更好利用浏览器缓存的效果
     */
    splitChunks: {
        // 对同步和异步模块都进行分割
        chunks: 'all',
        // 每次异步加载的最大并行请求数
        maxAsyncRequests: 10,
        // 入口点的最大并行请求数
        maxInitialRequests: 10,
        cacheGroups: {
            // 第三方依赖库
            vendor: {
                test: /[\\/]node_modules[\\/]/,
                // 模块名称
                name: 'vendor',
                // 优先级,数字越大,优先级越高
                priority: 20,
                // 强制执行
                enforce: true,
                // 复用已有的公共模块
                reuseExistingChunk: true
            },
            // 公共模块
            common: {
                // 模块名称
                name: 'common',
                // 被两处引用即被归为公共模块
                minChunks: 2,
                // 最小分割文件大小
                minSize: 1,
                // 优先级,数字越大,优先级越高
                priority: 10,
                // 复用已有的公共模块
                reuseExistingChunk: true
            }
        }
    }
}

生产环境配置

使用happypack 以及thread-loader 实现webpack的多线程打包

js 复制代码
const merge = require('webpack-merge')
const path = require('path')
const HappyPack = require('happypack')
const os = require('os')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')
const HtmlWebpackInjectAttributesPlugin = require('html-webpack-inject-attributes-plugin')
const TerserPlugin = require('terser-webpack-plugin')

// 多线程 build 设置
const happypackCommonConfig = {
    debug: false,
    threadPool: HappyPack.ThreadPool({ size: os.cpus().length })
}

// 基类配置
const baseConfig = require('./webpack.base.js')

// 配置生产环境webpack配置
const webpackConfig = merge.smart(baseConfig, {
// 指定生产环境
mode: 'production',
// 生产环境output配置
output: {
    filename: 'js/[name]_[chunkhash:8].bundle.js',
    path: path.join(process.cwd(), './app/public/dist/prod'),
    publicPath: '/dist/prod',
    crossOriginLoading: 'anonymous'
},
module: {
    rules: [
        {
            test: /\.css$/,
            use: [MiniCssExtractPlugin.loader, 'happypack/loader?id=css']
        },
        {
            test: /\.js$/,
            include: [path.resolve(process.cwd(), './app/pages')],
            use: [
                {
                    loader: 'thread-loader',
                    options: {
                        workers: os.cpus().length
                    }
                },
                'swc-loader'
            ]
        },
        {
            test: /\.less$/,
            use: [
                MiniCssExtractPlugin.loader,
                'css-loader',
                {
                    loader: 'thread-loader',
                    options: {
                        workers: os.cpus().length
                    }
                },
                'less-loader'
            ]
        }
    ]
},
// webpack不会有大量 hints 信息, 默认为warning
performance: {
    hints: false
},
cache: {
    type: 'filesystem'
},
plugins: [
    // 每次build前,清空打包后的产物
    new CleanWebpackPlugin(['public/dist'], {
        root: path.resolve(process.cwd(), './app/'),
        exclude: [],
        verbose: true,
        dry: false
    }),
    // 提取公共css,有效利用缓存,非公共部分使用inline
    new MiniCssExtractPlugin({
        chunkFilename: 'css/[name]_[contenthash:8].css'
    }),
    // 优化并压缩css资源
    new CssMinimizerWebpackPlugin(),
    // 多线程打包CSS,加快打包速度
    new HappyPack({
        ...happypackCommonConfig,
        id: 'css',
        loaders: [
            {
                path: 'css-loader',
                options: {
                    importLoader: 1
                }
            }
        ]
    }),
    // 浏览器请求资源时不发送用户的身份凭证
    new HtmlWebpackInjectAttributesPlugin({
        crossorigin: 'anonymous'
    })
],
optimization: {
    // 使用TerserPlugin压缩的并发和缓存,提升压缩性能
    // 清除console.log
    minimize: true,
    minimizer: [
        new TerserPlugin({
            terserOptions: {
                compress: {
                    // 去掉console内容
                    drop_console: true
                }
            }
        })
    ],
    runtimeChunk: true
}
})

module.exports = webpackConfig

开发环境

HMR原理

在开发环境中,不仅需要走完解析引擎编译、打包、压缩这一系列流程之外,为了能够实时感知到代码变化后页面的变化,我们需要引入一个服务器,这个服务器需要具备以下两个能力:

  1. 监听能力 -> 能够实时监听到文件的变化
  2. 通知能力 -> 在监听到文件变化后通知浏览器刷新页面

除此以外,与生产环境不同的是,开发环境最终只会产出模版文件(比如.tpl文件),剩余的资源文件(如js、css)将会以内存的方式存在于服务器当中,这种方式的好处是节省将文件写入时间,提高开发效率

webpack HMR实现
js 复制代码
// 本地开发 devServer
const express = require('express')
const webpack = require('webpack')
const { webpackConfig, DEV_SERVER_CONFIG } = require('./config/webpack.dev')
const consoler = require('consoler')
const DevMiddleware = require('webpack-dev-middleware')
const HotMiddleware = require('webpack-hot-middleware')
const path = require('path')

const app = express()

const compiler = webpack(webpackConfig)

consoler.info('请等待webpack初次构建完成提示...')

// 指定静态文件目录
app.use(express.static(path.resolve(__dirname, '../public/dist')))
// 引用DevMiddleware 中间件 监控文件改动
app.use(
DevMiddleware(compiler, {
    // 落地文件
    writeToDisk: filePath => filePath.endsWith('.tpl'),
    // 资源路径
    publicPath: webpackConfig.output.publicPath,
    // headers 配置(解决跨域问题)
    headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Headers': 'X-Requested-With, Content-Type, Authorization',
        'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS,PATCH'
    },
    // 打印日志
    stats: {
        colors: true
    }
})
)
// 引用HotMiddleware 中间件 实现热更新
app.use(
HotMiddleware(compiler, {
    path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
    log: () => {}
})
)

app.listen(DEV_SERVER_CONFIG.PORT, () => {
console.log(`app running at http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}`)
})
开发环境配置
js 复制代码
const merge = require('webpack-merge')
const webpack = require('webpack')
const path = require('path')

// 基类配置
const baseConfig = require('./webpack.base.js')

// dev-server 配置
const DEV_SERVER_CONFIG = {
    HOST: '127.0.0.1',
    PORT: 5100,
    HMR_PATH: '__webpack_hmr',
    TIMEOUT: 20000
}

// 开发阶段 entry 配置需要加入hmr

Object.keys(baseConfig.entry).forEach(key => {
    // 第三包不作为hmr入口
    if (key !== 'vendor') {
        baseConfig.entry[key] = [
            // 主入口文件
            baseConfig.entry[key],
            // hmr更新入口 官方指定的hmr路径
            `webpack-hot-middleware/client?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMR_PATH}&timeout=${DEV_SERVER_CONFIG.TIMEOUT}&reload=true`
        ]
    }
})

const webpackConfig = merge.smart(baseConfig, {
    // 指定开发环境
    mode: 'development',
    // 开发环境output配置
    output: {
        filename: 'js/[name]_[chunkhash:8].bundle.js',
        path: path.resolve(process.cwd(), './app/public/dist/dev/'),
        publicPath: `http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/public/dist/dev/`,
        globalObject: 'this'
    },
    // sourcemap 开发工具,呈现代码的映射关系,便于在开发过程中调试代码
    devtool: 'eval-cheap-module-source-map',
    // 开发阶段插件
    plugins: [
        // 用于实现热模块替换
        // 模块热替换运行应用程序运行时替换模块
        // 极大提升开发效率,因为能让应用程序保持运行状态
        new webpack.HotModuleReplacementPlugin({
            multiStep: false
        })
    ]
})

module.exports = {
    // devServer配置,给dev.js使用
    DEV_SERVER_CONFIG,
    // webpack配置
    webpackConfig
}

注:本文内容均来源于抖音哲玄前端大前端全栈实践课程,结合我的理解进行的思路整理,如有误,欢迎各位大佬指正。

相关推荐
旭久6 分钟前
react+antd封装一个可回车自定义option的select并且与某些内容相互禁用
前端·javascript·react.js
是纽扣也是烤奶10 分钟前
关于React Redux
前端
阿丽塔~13 分钟前
React 函数组件间怎么进行通信?
前端·javascript·react.js
冴羽37 分钟前
SvelteKit 最新中文文档教程(17)—— 仅服务端模块和快照
前端·javascript·svelte
uhakadotcom38 分钟前
Langflow:打造AI应用的强大工具
前端·面试·github
前端小张同学1 小时前
AI编程-cursor无限使用, 还有谁不会🎁🎁🎁??
前端·cursor
yanxy5121 小时前
【TS学习】(15)分布式条件特性
前端·学习·typescript
uhakadotcom1 小时前
Caddy Web服务器初体验:简洁高效的现代选择
前端·面试·github
前端菜鸟来报道1 小时前
前端react 实现分段进度条
前端·javascript·react.js·进度条