前言
Web前端应用开发主要术语有:HTML、CSS、DOM、JavaScript,前三者的标准由W3C制定,后者由ECMA制定,可以认为DOM规范提供了前端页面拥抱JS技术的接口。一般Web应用运行在浏览器中,由浏览器内核提供的渲染引擎、JS执行引擎共同完成程序的加载。不同的浏览器对HTML、CSS、JS规范的支持有所差异,所以前端的这三部分都存在兼容性问题。另外,浏览器厂商实现JS操DOM的特定API被称作BOM,这一部分在不同浏览器中更是存在很多差异。
自前后端分离后,前端技术发展的热火朝天,越来越多的技术名词被创造并实践:MVVM、Hybrid、WebAssembly、PWA、V8、NodeJS、ReactNative、QuickJS...移动端对此乐此不疲
前端技术大爆炸时代,相信很多开发朋友都想寻求一种最合理有效的应用构建方式,目前看webpack构建工具最为流行。下面就说说怎么从新开始撸一套基于webpack4的健壮的项目构建方案。
基于webpack的项目构建方案
需求
- 基础功能:图片、字体、样式处理
- 扩展支持:less、sass、ts、js新特性
- 按要求格式输出
- dist/[模块名]
- assets
- font
- image
- css
- js
- index.html
- 差异化构建:多环境构建、单页多页应用混合构建
- 适配要求:移动端自适应,浏览器兼容性
需求分析
下面按需求逐条分析该如何实现
基础功能:图片、字体、样式处理
给出webpack/module/rules配置
{
test: /\.(jpg|png|svg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
esModule: false,
name: '[name].[hash].[ext]',
outputPath: 'assets/image/',
limit: 2048
}
}
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
{
loader: 'url-loader',
options: {
esModule: false,
name: '[name].[hash].[ext]',
outputPath: 'assets/font/',
limit: 2048
}
}
]
},
{
test: /\.css$/,
use: [
miniCssConfig,
'css-loader',
postCssConfig
]
},
{
test: /\.less$/,
use: [
miniCssConfig,
'css-loader',
postCssConfig,
'less-loader'
]
},
{
test: /\.scss$/,
use: [
miniCssConfig,
'css-loader',
postCssConfig,
'sass-loader'
]
},
这里解释下处理图片、字体为何使用url-loader而不是file-loader(简而言之就是前者更好)
如果页面图片较多,发很多 http 请求,会降低页面性能。这个问题可以通过 url-loader 解决。url-loader 会将引入的图片以 base64 编码并打包到文件中,最终只需要引入这个dataURL 就能访问图片了。当然,如果图片较大,编码会消耗性能。因此 url-loader 提供了一个 limit 参数,小于 limit 字节的文件会被转为 base64,大于 limit 的会使用 file-loader 的参数进行命名,并把图片 copy 到指定文件夹内。
less-loader用于将less样式文件转为css文件,使用前需引入less库,sass-loader同理。
这部分依赖的库整理如下:
- url-loader
- less
- less-loader
- node-sass
- sass-loader
扩展支持:less、sass、ts
显然前面一节已经提到了如何支持less、sass,这里只说如何支持ts开发项目。有两种方式可实现ts开发:ts-loader方式和@babel/preset-typescript方式——可以理解为webpack方案和babel方案,我这里选择后者,所以得先谈谈babel的配置。
Babel is a JavaScript compiler. Use next generation JavaScript, today.
引自babel官网首页,第二句话是说babel可以让我们使用下一代js语法。JS被称作动态脚本语言,动态是指在编译执行时才确定变量类型。要想提前享福,得啃硬骨头——babel会接收当前代码得到AST重新生成代码,说babel是js编译器也不为过。
给出babel配置如下
module.exports = function (api) {
api.cache(true)
const presets = [
[
"@babel/preset-env",
{
modules: false,
useBuiltIns: "entry",
corejs: {
version: 3,
proposals: true
}
}
],
[
"@babel/preset-typescript"
]
]
const plugins = [
["transform-es2015-modules-commonjs"],
["@babel/plugin-proposal-class-properties", { "loose": true }],
'dynamic-import-node'
]
return {
presets,
plugins
}
}
babel的配置一般由preset(预设)和plugin(插件/扩展)两部分构成,前者决定项目中JS的起点(躯干),后者决定项目JS的丰度(四肢),可以说两者相辅相成。
添加预设@babel/preset-env可以快速实现下一代js语法支持,它本身也支持一些个性化配置:比如使用core-js3实现新的API,结合.browserslistrc设定浏览器支持范围,useBuiltIns决定了babel使用js垫片的方式。
同样地,添加预设@babel/preset-typescript便可一键支持ts开发,毋庸置疑,这个预设底层肯定也是转换ts为js了的。
对于js新特性,包括新语法和新API两部分,新语法由babel搞定,core-js实现后者。
在实际编码过程中可能会遇到一些语法上的问题:
按要求格式输出
再来回顾下要求的格式
- dist/[模块名]
- assets
- font
- image
- css
- js
- index.html
也就是说图片、字体、样式、js文件存放在不同目录。
图片、字体输出配置
回顾基础功能:图片、字体、样式处理,可以发现rules中图片和文字输出目录的相关配置
outputPath: 'assets/image/',
outputPath: 'assets/font/',
js文件的输出配置
output: {
filename: isPrd ? 'js/[name]-[chunkhash].js' : 'js/[name]-[hash].js',
path: pathSolve(`dist/${targetEntryName}`)
},
css文件输出配置
css呢?别急。实践过的人会发现webpack默认是将css包含在js文件中的,所以需要使用插件mini-css-extract-plugin将其提取到单独的文件中。本地开发模式下为了加快构建速度,可以不用考虑这一点。
mini-css-extract-plugin的使用
插件mini-css-extract-plugin的配置分两部分:loader和plugins。在发布模式下,将会使用MiniCssExtractPlugin.loader替换style-loader处理样式,因此loader部分的配置类似如下:
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../',
reloadAll: true
}
plugins中配置如下:
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[id].[contenthash:8].css',
ignoreOrder: true
})
所以plugin中指定了提取出的css的目录和文件名,当然也可以通过loader配置中的publicPath也可以指明目录,比如../css
使用这个插件遇到的一些问题这里罗列一下:
- css文件中的url路径问题 参考这里
- MiniCssExtractPlugin使用时css文件中的url路径问题 参考这里
- MiniCssExtractPlugin 会抛出相同样式在不同文件中引入顺序问题 参考这里
css压缩优化
使用OptimizeCSSAssetsPlugin压缩css文件,它的配置如下
new OptimizeCSSAssetsPlugin({
cssProcessor: require('cssnano'),
cssProcessorPluginOptions: {
preset: [
'default',
{
discardComments: { removeAll: true },
mergeLonghand: false
}
],
},
canPrint: true
})
使用时可能遇到类似的问题:my.oschina.net/MyItStudy/b…
差异化构建:多环境构建、单页多页应用混合构建
这句话的言外之意是一套构建方案适于各种项目:基于应用形态(单页、多页)、基于业务类型(A、B两个不同的项目)、基于发布环境(测试环境、线上环境)等
基于应用形态、业务类型构建项目
这两种场景可以综合为体现为模块化开发,创建合理的工程目录便可实现,如下目录结构:
- src(源码目录)
- assets(字体、图片、全局样式等)
- font
- image
- style
- entry (入口文件)
- global (全局功能:统跳协议、用户信息读取更新等)
- http (网络请求实现)
- page (页面)
- widget (基础组件)
- router (页面路由实现)
- storage(本地存储)
- store (应用状态数据管理)
- util (工具类)
- assets(字体、图片、全局样式等)
基于发布环境多样性构建项目
这种情况考虑实际项目中的服务器环境有多套,还要考虑方便自动化构建工具的使用,我们可以通过区分 webpack配置来实现,如下构建配置:
- build(构建目录)
- env.config.js(发布环境配置)
- webpack.config.base.js (通用webpack配置)
- webpack.config.dev.js (本地开发webpack配置)
- webpack.config.prd.js (发布环境webpack配置)
env.config.js中配置一些项目环境参数
const configA = {
local: { // 本地开发
BASE_URL: "http://xxx:8081",
SERVER_ENV: "local",
NODE_ENV: "development",
PROXY_CONTEXT: ["/", "/system/api", "/media/api"]
},
prd: { // 发布模式-生产环境
BASE_URL: "http://xxx:8081",
SERVER_ENV: "prd",
NODE_ENV: "production"
}
}
package.json中的构建命令如下
"serve": "cross-env NODE_ENV=development webpack-dev-server --hot --config build/webpack.config.dev.js",
"build:dev": "cross-env SERVER_ENV=dev npm run build",
"build:test": "cross-env SERVER_ENV=test npm run build",
"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.prd.js",
其中build:dev、build:test便是针对发布模式下各种环境的区分,这两种命令也可以提取为npm 命令参数比如叫senv,然后在env.config.js中接收这个参数,匹配config下local、prd等属性。
对于多个项目(独立模块)开发的场景,在env.config.js添加新的项目配置
const configB = {
local: { // 本地开发
xxx
},
prd: { // 发布模式-生产环境
xxx
}
}
这时,需要为npm命令增加一个参数如starget,用来指定当前构建哪个项目(独立模块),从而确定使用哪套配置
const npmConfigArgV = JSON.parse(process.env.npm_config_argv)
const cooked = npmConfigArgV.cooked
console.log('命令参数列表', cooked)
let starget = ''
if (Array.isArray(cooked)) {
const stIndex = cooked.indexOf('--starget')
if (stIndex > -1) {
starget = cooked[stIndex + 1]
}
}
then choose configA or configB
适配要求:移动端自适应,浏览器兼容性
页面的自适应采用基于rem的适配方案,引入amfe-flexible
对于css的适配方案,采用postcss-loader、autoprefixer、postcss-pxtorem相结合的方案
入口文件引入
import 'amfe-flexible'
postcss配置
const postCssConfig = {
loader: 'postcss-loader',
options: {
plugins: [
require('autoprefixer'),
require('postcss-pxtorem')(
{
rootValue: 37.5,
propList: ['*']
}
)
]
}
}
这样便可愉快地玩耍了,至于项目中使用vue或者react开发,可随意选择,当然在现有基础上还需要添加一些配置。对于vue,主要是添加vue-loader、vue-template-compiler,对于react,主要是添加@babel/preset-react,webpack配置如下:
{
test: /\.(js|jsx|ts|tsx)$/,
use: [
'thread-loader',
'babel-loader'
],
exclude: /node_modules/
},
{
test: /\.vue$/,
use: [
'vue-loader'
],
}