前言
第一次接触webpack是因为使用了Vue,因为Vue的脚手架就是使用webpack搭建的。 一开始我以为webpack就是为了打包单页而生的,但后来想想,这么好的打包方案只用在单页上是不是很浪费资源呢? 如果webpack能用在传统的多页面上,一开始会不会效率更高? 幸运的是,很多优秀的后端开发人员已经写了很多demo和文章供人们学习。 我也写了一个小项目,希望对大家学习webpack有帮助。
好吧,虽然我上面说的都是废话,但是接下来我会附上项目地址和干货,这样更好吃。
webpack 多页面
项目解决的问题
src目录对应dist目录
当我们使用webpack打包多个页面时,我们希望src目录对应打包后的dist目录,如上图所示。 开发框架按照页面模块的思想构建,然后通过webpack打包生成传统页面的结构。
/** * 创建打包路径 */ const createFiles = function() { const usePug = require('../config').usePug; const useTypeScript = require('../config').useTypeScript; const path = require('path'); const glob = require('glob'); const result = []; const type = usePug ? 'pug' : 'html'; const scriptType = useTypeScript ? 'ts' : 'js'; const files = glob.sync(path.join(__dirname, `../src/views/**/*.${type}`)); for (file of files) { result.push({ name: usePug ? file.match(/w{0,}(?=.pug)/)[0] : file.match(/w{0,}(?=.html)/)[0], templatePath: file, jsPath: file.replace(type, scriptType), stylePath: file.replace(type, 'stylus') }); } return result; };
使用该方法,我们可以获取待打包的文件路径(方法中获取文件路径的模块也可以使用fs模块),根据打包的文件路径,我们可以使用html-webpack-plugin来实现多页包装。
由于 html-webpack-plugin 的每个对象实例仅针对/生成一个页面,因此如果我们制作多页面应用程序,我们需要配置多个 html-webpack-plugin 对象实例:
const plugins = function() { const files = createFiles(); const HtmlWebpackPlugin = require('html-webpack-plugin'); const path = require('path'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); let htmlPlugins = []; let Entries = {}; files.map(file => { htmlPlugins.push( new HtmlWebpackPlugin({ filename: `${file.name}.html`, template: file.templatePath, chunks: [file.name] }) ); Entries[file.name] = file.jsPath; }); return { plugins: [ ...htmlPlugins, new ExtractTextPlugin({ filename: getPath => { return getPath('css/[name].css'); } }) ], Entries }; };
由于我使用了ExtractTextPlugin,这段CSS代码最终会以CSS文件的形式生成到它所属的chunk的目录中。
模板引擎
为每个页面维护相同的 UI 布局是非常困难的。 如果UI稍微改一下,就得去每个页面去改。 非常麻烦而且容易错过。 怎么能破呢?
考虑到这个问题,项目引入并使用了pug模板引擎。
现在,我们可以使用 pug 的功能来创建共享组件:
演示.pug
p 这是一个共享组件
然后,当你需要使用这个公共组件时,你可以导入它:
include 'demo.pug'
此外,您还可以使用哈巴狗的所有特殊功能。
在webpack中配置pug也很简单,首先安装:
npm i --save-dev pug pug-html-loader
然后将所有.html后缀改为.pug后缀,并使用pug句型。
然后减少规则中的另一个配置
{ test: /.pug$/, use: 'pug-html-loader' }
同时,plugins对象中index.html使用的HtmlWebpackPlugin中的模板也应该改为index.pug。
Webpack 集成 eslint
先放出配置代码:
if (useEslint) { loaders.push({ test: /.js$/, loader: 'eslint-loader', enforce: 'pre', include: [path.resolve(__dirname, 'src')], options: { formatter: require('stylish') } }); }
通过将ESLint与webpack集成,我们可以保证编译生成的代码没有语句错误,符合编码标准; 但在开发过程中,可能要等到编译的时候才意识到问题。
因此,我建议可以将ESLint集成到编辑器或IDE中。 比如我自己使用vs code的时候,可以使用一个叫做Eslint的插件。 一旦我写出有问题的代码,它就会立即被标记。
开发环境和生产环境
首先,在阅读webpacl项目时webpack 多个页面,一般都会先看package.json文件。因为当你在命令行输入下一条命令时
npm run dev
webpack会找到package.json文件中的script属性,依次分析命令。 可以看到这条命令会被相应执行
复制代码代码如下:
nodemon --watch build/ --exec "cross-env NODE_ENV=development webpack-dev-server --color --progress --config build/webpack.dev.js"
同样,编写命令时
npm run build
脚本将被执行
ross-env NODE_ENV=production webpack --config build/webpack.prod.js
这样可以区分开发环境和生产环境。
虽然我们会对环境进行区分webpack 多个页面,但基于不重复的原则,将两个环境的项目的通用配置集成到(build/webpack.base.js)文件中。然后将配置与webpack-merge 插件的帮助
在 webpack 中使用 jquery
在webpack中使用jquery也很简单,我们可以减少loader中的一个配置:
if (useJquery) { loaders.push({ // 通过require('jquery')来引入 test: require.resolve('jquery'), use: [ { loader: 'expose-loader', // 暴露出去的全局变量的名称 随便你自定义 options: 'jQuery' }, { // 同上 loader: 'expose-loader', options: '$' } ] }); }
那么当你需要在js文件中使用jq时,只需引用暴露的变量名即可:
import $ from 'jQuery';
在 webpack 中使用 typescript
在webpack中使用jquery也很简单,我们可以减少loader中的一个配置:
if (useTs) { loaders.push({ test: /.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }); }
然后将js文件改为ts.
以上就是本文的全部内容。 希望对您的学习有所帮助,也希望您多多支持脚本之家。
carletsky.github.io/2019/02/19/webpack-bundling-libraries-with-dynamic-imports/
在编译库时,我们有时希望按需加载个别依赖项,例如,如果代码的运行环境不支持个别功能,则加载相关的Polyfill。
webpack作为目前最流行的打包工具,已经支持动态加载的功能。
本文将讨论一种用 webpack 打包动态加载解释器的方法。
请注意,本文专门针对一般作者。 如果读者正在编写应用程序,则无需往下读。
泛型示例
// my-lib.jsclass MyLib {
loadDeps() {
return new Promise((resolve, reject) => {
if (global.TextDecoder === undefined) {
return Promise.all([
import('./deps/text-encoding'),
import('./deps/other-dep.js'),
import('./deps/another-dep.js'),
]).then(resolve).catch(reject);
} else {
resolve();
}
});
}
initialize() {
this.decoder = new TextDecoder();
// ...
}}
该例程中有一个loadDeps方法,该方法会根据运行环境监控TextDecoder是否存在。 如果不存在,则会加载本地目录中的依赖项。
当我们将解释器发布到 npm 时,我们将像这样使用它:
// app.jsimport MyLib from 'my-lib';class App {
constructor() {
this.myLib = new MyLib();
}}const app = new App();
app.myLib.loadDeps().then(() => {
app.myLib.initialize();
console.log(app.myLib.decoder);});
这样我们的解释器无论在什么环境下都可以使用TextDecoder。
可是,事情真的会这么顺利吗?
问题
在发布到npm仓库之前,我们首先使用Webpack来构建项目。 这时,webpack会分析文件中的依赖路径,并将依赖文件生成到output.path中。 同时,import()方法已经被做成了webpack内部的方式,依赖路径也与output.publicPath相关。
假设我们的webpack配置是这样的:
// webpack.config.jsmodule.exports = {
output: {
filename: 'index.js',
chunkFilename: '[name].js',
path: 'dist/',
publicPath: '/',
libraryTarget: 'umd'
},
// ...};
之后输出这些文件:
// webpack outputBuilt at: 02/19/2019 5:08:41 PM
Asset Size Chunks Chunk Names
1.js 79 bytes 1 [emitted]
2.js 79 bytes 2 [emitted]
3.js 79 bytes 3 [emitted]
index.js 144 KiB 0 [emitted] main
Entrypoint main = index.js
那么当应用层调用app.myLib.loadDeps()时,加载依赖的路径会发生什么变化呢?
你猜对了,浏览器会尝试加载这个路径:
/1.js/2.js/3.js
但结果呢? 其实是404。因为网站根目录下没有这个文件,除非你把那些文件复制到网站根目录下。
同理,如果publicPath是相对路径(假设为''),那么请求的路径将是相对于当前URL的,即如果当前URL是/topics/:uid,那么请求的路径将是 /topics/ :uid/1.js。 除非当前目录存在这样的依赖,否则结果仍然是404。
事实上,我们还可以更改服务器配置,将 /1.js 和 **/1.js 都指向 /path/to/project/node_modules/my-lib/dist/1.js。
对于应用层来说webpack按需加载,改变一个库的服务器配置就变得很麻烦。
对于泛型来说,应该管理自己的依赖关系,不应该让应用层甚至服务器端配合。
解决方案
我们需要解决这个路径问题,既能保证结果正确,又能方便开发。
显然,这个问题是webpack打包时对import()的处理导致的。 如果我们不在编译时处理,而是在运行时处理,这不就达到目的了吗?
我们先规划一下dist/的目录结构,并更改webpack的配置:
const CopyWebpackPlugin = require('copy-webpack-plugin');module.exports = {
output: { ... },
plugins: [
new CopyWebpackPlugin([{
from: 'src/deps/',
to: '.'
}])
]}
CopyWebpackPlugin会将src/deps/中的文件复制到dist/目录下,然后dist/就会变成:
dist
├── 1.js├── 2.js├── 3.js├── text-encoding.js
├── other-dep.js
├── another-dep.js
└── index.js
下一步是让webpack不解析import(),这里有两种方法:
选项1:由应用层处理
这个方案实现起来很简单,只要让应用层调用import()即可。
// my-lib.jsclass MyLib {
constructor(options) {
this.onLoadDeps = options.onLoadDeps || null;
}
loadDeps() {
return new Promise((resolve, reject) => {
if (global.TextDecoder === undefined) {
if (this.onLoadDeps) {
this.onLoadDeps().then(resolve).catch(reject);
} else {
Promise.all([
import('./deps/text-encoding'),
import('./deps/other-dep.js'),
import('./deps/another-dep.js'),
]).then(resolve).catch(reject);
}
} else {
resolve();
}
});
}}// app.jsimport MyLib from 'my-lib';class App {
constructor() {
this.myLib = new MyLib({
onLoadDeps: () => Promise.all([
import('my-lib/dist/text-encoding'),
import('my-lib/dist/other-dep.js'),
import('my-lib/dist/another-dep.js'),
]);
});
}}
这些方法非常简单,但是不需要任何处理就可以在开发环境中正常运行。
缺点是,如果多个项目引用该例程,那么当解释器添加新的依赖项时webpack按需加载,所有引用该例程的项目都将不得不更改源代码。 这将是一件相当漫长的事情。
对于这个解决方案,虽然有一个变种,但是相对来说更加方便:
// my-lib.jsclass MyLib {
constructor(options) {
this.importPrefix = options.importPrefix || './deps';
}
loadDeps() {
return new Promise((resolve, reject) => {
if (global.TextDecoder === undefined) {
return Promise.all([
import(this.importPrefix + '/text-encoding'),
import(this.importPrefix + '/other-dep.js'),
import(this.importPrefix + '/another-dep.js'),
]).then(resolve).catch(reject);
} else {
resolve();
}
});
}}// app.jsimport MyLib from 'my-lib';class App {
constructor() {
this.myLib = new MyLib({ importPrefix: 'my-lib/dist' });
}}
注意:此变体可能会报告 Critical dependency:requestofdependencyisanexpression 错误。
选项 2:由泛型处理
这个方案稍微复杂一些。
如果你想让webpack不处理import(),那么你就不能让webpack解析富含import()的文件,也就是说,你需要将富含加载依赖的部分分离到另一个文件中。
// runtime.jsmodule.exports = {
onLoadDeps: function() {
return Promise.all([
import('my-lib/dist/text-encoding'),
import('my-lib/dist/other-dep.js'),
import('my-lib/dist/another-dep.js'),
]);
}}
注意:由于 webpack 不会解析这个文件,加载器也不会处理这个文件,所以最好在这个文件上使用 Node.js 原生支持的句型。
// my-lib.jsimport RUNTIME from './runtime';class MyLib {
loadDeps() {
return new Promise((resolve, reject) => {
if (global.TextDecoder === undefined) {
RUNTIME.onLoadDeps().then(resolve).catch(reject);
} else {
resolve();
}
});
}}
之后更改 webpack 配置:
module.exports = {
output: { ... },
module: {
noParse: /src/runtime/,
},
plugins: [ ... ]
}
这样,webpack在处理my-lib.js时会加载runtime.js,但不会解析它。 所以你会得到以下结果:
// dist/index.js/******/ ([/* 0 *//***/ (function(module, exports) {module.exports = {
onLoadDeps: function onLoadDeps() {
return Promise.all([import('my-lib/dist/text-encoding'), import('my-lib/dist/other-dep.js'), import('my-lib/dist/another-dep.js')]);
}};/***/ }),/* 1 *//***/ (function(module, __webpack_exports__, __webpack_require__) {// ...var MyLib =/*#__PURE__*/function () {
function MyLib() {}
var _proto = MyLib.prototype;
_proto.loadDeps = function loadDeps() {
var _this = this;
return new Promise(function (resolve, reject) {
if (global.TextDecoder === undefined) {
_runtime__WEBPACK_IMPORTED_MODULE_0___default.a.onLoadDeps().then(resolve).catch(reject);
} else {
resolve();
}
});
};
_proto.initialize = function initialize() {
this.decoder = new TextDecoder(); // ...
};
return MyLib;}();// ...
如果应用层引用了这个例程,那么webpack在打包应用的时候就会处理例程中的import(),和平时应用层的动态加载是一样的,里面的问题就迎刃而解了。
最后剩下的问题是,在开发环境中,我们还需要测试runtime.js,但此时导入的是('my-lib/dist/xxx'),肯定会报 Error: Cannotfindmodule 错误。
这时可以像解决方案1那样通过import(importPrefix+'/text-encoding')的形式来解决,也可以借助NormalModuleReplacementPlugin来解决。
// webpack.dev.jsmodule.exports = {
// ...
plugins: [
new webpack.NormalModuleReplacementPlugin(/my-lib/dist/(.*)/, function (resource) {
resource.request = resource.request.replace(/my-lib/dist/, '../src/deps')
}),
]}
这个插件可以改变重定向的资源,里面的配置是将my-lib/dist/*上面的资源重定向到../src/deps。
更详细的使用方法请参考官方文档NormalModuleReplacementPlugin。
注意:此插件最好仅在开发环境中使用。
这个方案看起来有点冗长,但最大的好处是可以将依赖管理交给泛型本身,无需应用层干预。 即使以后泛型改变了打包位置(不再是dist/),也不需要让应用层知道。