摘要:构建的基于的多页应用脚手架,本文聊聊本次项目中构建多页应用的一些心得体会。仓库构建的旧版多页应用构建的多页应用。例如多页应用中每个的值对应的文件。
Webpack构建的基于zepto的多页应用脚手架,本文聊聊本次项目中Webpack构建多页应用的一些心得体会。
1.前言由于公司旧版的脚手架是基于Gulp构建的zepto多页应用(有兴趣可以看看web-mobile-cli),有着不少的痛点。例如:
需要兼容低版本浏览器,只能采用promise,不能使用await、generator等。(因为babel-runtime需要模块化);
浏览器缓存不友好(只能全缓存而不是使用资源文件的后缀哈希值来达到局部缓存的效果);
项目的结构不友好(可以更好的结构化);
开发环境下的构建速度(内存);
Gulp插件相对Webpack少且久远,维护成本高等等。
这次升级有几个地方需要注意和改进:
项目旧代码尽量做到无缝转移;
资源文件的缓存;
组件式的组织目录结构。
Github仓库:
Gulp构建的旧版多页应用web-mobile-cli;
Webpack构建的多页应用web-mobile-webpack-cli。
Webpack的多页应用通过多入口entry和多实例html-webpack-plugin配合来构建,html-webpack-plugin的chunk属性传入对应entry的key就可以做到关联,例如:
module.exports = {
entry: {
pageOne: "./src/pageOne/index.js",
pageTwo: "./src/pageTwo/index.js",
pageThree: "./src/pageThree/index.js"
},
plugins: [
new HtmlWebpackPlugin({
filename: `pageOne.html`,
template: `./src/pageOne.html`,
chunks: ["pageOne"]
}),
new HtmlWebpackPlugin({
filename: `pageTwo.html`,
template: `./src/pageTwo.html`,
chunks: ["pageTwo"]
}),
new HtmlWebpackPlugin({
filename: `pageTwo.html`,
template: `./src/pageTwo.html`,
chunks: ["pageTwo"]
})
]
}
那么问题来了,开发新的页面每次都得添加岂不是很麻烦。这里推荐神器glob根据正则规则匹配。
const glob = require("glob")
module.exports = {
entry: glob.sync("./src/js/*.js").reduce((pre, filepath) => {
const tempList = filepath.split("src/")[1].split(/js//)
const filename = `${tempList[0]}${tempList[1].replace(/.js/g, "")}`
return Object.assign(pre, {[filename]: filepath})
}, {}),
plugins: [
...glob.sync("./src/html/*.ejs").map((filepath, i) => {
const tempList = filepath.split("src/")[1].split(/html//)
const fileName = tempList[1].split(".")[0].split(/[/|//||]/g).pop()
const fileChunk = `${tempList[0]}${fileName}`
return new HtmlWebpackPlugin({
filename: `${fileChunk}.html`,
template: filepath,
chunks: [fileChunk]
})
})
]
}
3.模板
项目没有直接使用html,而是使用了ejs作为模板,这里有至少两个好处:
把公共的代码抽离出来;
传入公共的变量。
// header.ejs
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%= title %>title>
head>
// index.ejs
<html lang="en">
<% include ./header.ejs %>
<body>
body>
<script src="<%= publicPath %>lib/zepto.js">script>
html>
<% include ./header.ejs %>就是引用了header.ejs文件,<%= title %>和<%= publicPath %>是我在配置文件定义的两个变量,publicPath是为了统一cdn缓存服务器的域名,非常有用。
4.垫片项目中使用了zepto,所以需要垫片,所谓垫片就是shim 预置依赖,即全局依赖。
webpack compiler 能够识别遵循 ES2015 模块语法、CommonJS 或 AMD 规范编写的模块。然而,一些 third party(第三方库) 可能会引用一些全局依赖(例如 jQuery 中的 $)。因此这些 library 也可能会创建一些需要导出的全局变量。这些 "broken modules(不符合规范的模块)" 就是 shim(预置依赖) 发挥作用的地方。
垫片有两种方式:
传统方式的垫片就是在html文件中,所有引用的js文件的最前面引用的文件(例如zepto);
Webpack配置shim预置依赖。
最终我选择了Webpack配置shim预置依赖这种方式,因为:
传统的方式需要每个页面都手动引入(虽说搭配ejs可以抽离出来成为公共模块,但还是需要每个页面手动引入公共模块);
传统的方式需要多发一次请求去请求垫片;
Webpack可以把所有第三方插件的代码都拆分打包成为一个独立的chunk,只需一个请求。
module.exports = {
entry: {...},
module: {
rules: [
{
test: require.resolve("zepto"),
use: "imports-loader");
}
]
},
plugins: [
new webpack.ProvidePlugin({$: "zepto"})
]
}
5.拆分
一般来讲Webpack的配置entry中每个key就对应输出一个chunk,那么该项目中会提取这几类chunk:
页面入口(entry)对应的chunk;
common:多次引用的公共文件;
vender:第三方依赖;
manifest:Webpack运行时(runtime)代码,它存储着Webpack对module和chunk的信息。
module.exports = {
entry: {...},
module: {...},
plugins: [],
optimization: {
runtimeChunk: {
name: "manifest"
},
splitChunks: {
cacheGroups: {
vendors: {
test: /[/]node_modules[/]/,
chunks: "all",
name: "vendors",
filename: "js/vendors.[contenthash:8].js",
priority: 2,
reuseExistingChunk: true
},
common: {
test: /.m");,
chunks: "all",
name: "common",
filename: "js/common.[contenthash:8].js",
minSize: 0,
minChunks: 2,
priority: 1,
reuseExistingChunk: true
}
}
}
}
}
这里注意的有两点:
优先顺序:第三方插件的priority比common代码的priority大;
提取common代码:minChunks为引用次数,我设置为引用2次即提取为公共代码。minSize为最小字节,设置为0。
缓存的目的是为了提高加载速度,Webpack在缓存方面已经是老生常谈的了,每个文件赋予唯一的hash值,只有更新过的文件,hash值才改变,以达到整体项目最少文件改动。
6.1 hash值Webpack中有三种hash值:
hash:全部文件同一hash,一旦某个文件改变,全部文件的hash都将改变(同一hash不满足需求);
chunkhash:根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值(问题是css作为模块import到JavaScript文件中的,它们的chunkhash是一致的,一旦改变js文件,即使import的css文件内容没有改变,其chunkhash值也会一同改变,不满足需求);
contexthash:只有模块的内容变了,那么hash值才改变(采用)。
module.exports = {
entry: {
pageOne: "./src/pageOne/index.js",
pageTwo: "./src/pageTwo/index.js",
pageThree: "./src/pageThree/index.js"
},
output: {
path: "src",
chunkFilename: "j[name].[contenthash:8].js",
filename: "[name].[contenthash:8].js"
},
plugins: [
new HtmlWebpackPlugin({
filename: `pageOne.html`,
template: `./src/pageOne.html`,
chunks: ["pageOne"]
}),
new HtmlWebpackPlugin({
filename: `pageTwo.html`,
template: `./src/pageTwo.html`,
chunks: ["pageTwo"]
}),
new HtmlWebpackPlugin({
filename: `pageTwo.html`,
template: `./src/pageTwo.html`,
chunks: ["pageTwo"]
})
]
}
6.2 module id
仅仅使用contexthash还不足够,每当import的资源文件顺序改变时,chunk依然会改变,目的没有达成。要解决这个问题首先要理解module和chunk分别是什么,简单理解:
module:一个import对应一个module(例如:import zepto from "zepto"中的zepto就是一个module);
chunk:根据配置文件打包出来的包,就是chunk。(例如多页应用中每个entry的key值对应的文件)。
因为Webpack内部维护了一个自增的id,依照顺序赋予给每个module,每当新增或者删减导致module的顺序改变时,受影响的chunk的hash值也会改变。解决办法就是使用唯一的hash值替代自增的id。
module.exports = {
entry: {...},
module: {...},
plugins: [],
optimization: {
moduleIds: "hashed"
}
}
7.优化
优化的目的是提高执行和打包的速度。
7.1 查找路径告诉Webpack解析模块时应该搜索的目录,缩小编译范围,减少不必要的编译工作。
const {resolve} = require("path")
module.exports = {
entry: {...},
module: {...},
plugins: [],
optimization: {...},
resolve: {
alias: {
"@": resolve(__dirname, "../src"),
},
modules: [
resolve("src"),
resolve("node_modules"),
]
}
}
7.2 指定目录
指定loader的include目录,作用是缩小编译范围。
const {resolve} = require("path")
module.exports = {
entry: {...},
module: {
rules: [
{
test: /.css$/,
include: [
resolve("src"),
],
use: ["style-loader", "css-loader"]
}
]
},
plugins: [],
optimization: {...},
resolve: {...}
}
7.3 babel缓存目录
babel-loader开始缓存目录cacheDirectory。
const {resolve} = require("path")
module.exports = {
entry: {...},
module: {
rules: [
{
test: /.m");,
exclude: /(node_modules|bower_components)/,
include: [
resolve("src"),
],
use: {
loader: "babel-loader",
options: {
cacheDirectory: true,
presets: ["@babel/preset-env"],
plugins: ["@babel/plugin-transform-runtime"]
}
}
}
]
},
plugins: [],
optimization: {...},
resolve: {...}
}
7.4 插件TerserJSPlugin
TerserJSPlugin插件的作用是压缩JavaScript,优化的地方是开启缓存目录和开启多线程。
const {resolve} = require("path")
module.exports = {
entry: {...},
module: {...},
plugins: [],
optimization: {
minimizer: [
new TerserJSPlugin({
parallel: true,
cache: true,
})
]
},
resolve: {...}
}
8.总结
通过这次学习Webpack到升级脚手架,对前端工程化有了进一步的了解,也感受到了Webpack4带来的开箱即用,挺方便的。
参考文章:
Webpack官方文档
【实战】webpack4 + ejs + express 带你撸一个多页应用项目架构
基于 webpack 的持久化缓存方案
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/6847.html
摘要:然而在某些特殊的应用场景之中,则需要使用到传统的多页应用。在使用进行项目工程化构建时,也需要对应到调整。配置入口设置多页应用的打包会对应多个入口文件,以及多个模版文件。方法一使用的文件系统。组合如下完整可查看多页应用 背景 随着react, vue, angular 三大前端框架在前端领域地位的稳固,SPA应用正在被应用到越来越多的项目之中。然而在某些特殊的应用场景之中,则需要使用到传...
摘要:原文地址如果您对本系列文章感兴趣,欢迎关注订阅这里前言上文多页应用架构系列十二利用生成普通网页页面模板我们基本上已经搞清楚如何利用来生成普通网页页面模板,本文将以我的脚手架项目介绍如何在这基础上搭建一套简单的模板布局系统。 本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。原文地址:https://segmentfault.com/a/1190000007...
摘要:回到纯静态页面开发阶段,让页面不需要后端渲染也能跑起来。改造开始本文着重介绍如何将静态页面改造成后端渲染需要的模板。总结在后端渲染的项目里使用多页应用架构是绝对可行的,可不要给老顽固们吓唬得又回到传统前端架构了。 本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。原文地址:https://segmentfault.com/a/119000000820338...
摘要:回到纯静态页面开发阶段,让页面不需要后端渲染也能跑起来。改造开始本文着重介绍如何将静态页面改造成后端渲染需要的模板。总结在后端渲染的项目里使用多页应用架构是绝对可行的,可不要给老顽固们吓唬得又回到传统前端架构了。 本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。原文地址:https://segmentfault.com/a/119000000820338...
摘要:本文首发于的技术博客实用至上,非经作者同意,请勿转载。原文地址如果您对本系列文章感兴趣,欢迎关注订阅这里这系列文章讲什么本系列文章主要介绍如何用这一当前流行的构建工具来设计一个多页应用的架构。 本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。原文地址:https://segmentfault.com/a/1190000006843916如果您对本系列文章...
阅读 2232·2021-11-22 09:34
阅读 1984·2021-09-22 15:22
阅读 1963·2019-08-29 15:05
阅读 2080·2019-08-26 10:43
阅读 3388·2019-08-26 10:26
阅读 836·2019-08-23 18:29
阅读 3498·2019-08-23 16:42
阅读 1976·2019-08-23 14:46