elpis之前端工程化
上文回顾
上篇文章当中,我们完成了elpis-core 的开发以及应用,接下来我们将继续进行elpis 的后续开发。本文需要完成的是前端工程化相关的内容。
前端工程化的概念
前端工程化 是个很大的概念,他是我们前端开发的一个总体的思想,包含模块化、包管理器等东西,但单从框架的角度出发(vue/react),前端工程化代表的是类似于elpis-core 的一个解析引擎,他将浏览器除JavaScript、CSS、HTML以外的文件通过类似于elpis-code 中的loader转化为浏览器认识的JavaScript文件。
如图所示,当我们通过框架写完业务代码之后,解析引擎通过一系列操作最终生成浏览器认识的html、css、js以及各种类型的图片文件。最终将产物文件交予服务器,服务器最终在浏览器输出用户可浏览的前端网页。
elpis的前端解析引擎
为什么是webpack
- webpack的官方文档比较清晰明了
- webpack的社区生态非常完善
- 本人对于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原理
在开发环境中,不仅需要走完解析引擎编译、打包、压缩这一系列流程之外,为了能够实时感知到代码变化后页面的变化,我们需要引入一个服务器,这个服务器需要具备以下两个能力:
- 监听能力 -> 能够实时监听到文件的变化
- 通知能力 -> 在监听到文件变化后通知浏览器刷新页面
除此以外,与生产环境不同的是,开发环境最终只会产出模版文件(比如.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
}
注:本文内容均来源于抖音哲玄前端 的大前端全栈实践课程,结合我的理解进行的思路整理,如有误,欢迎各位大佬指正。