首页
关于
Search
1
分享一些收集的Sync Key
5,508 阅读
2
mysql错误ERROR 1130 (HY000): Host 'localhost' is not allowed to connect to this MySQL server
1,634 阅读
3
对比win10系统上的三种软件包管理器scoop、chocolatey、winget
1,626 阅读
4
Resilio Sync 许可证下载
1,594 阅读
5
阿里云盘资源分享
1,249 阅读
前端
CSS
NodeJS
Javascript
小程序
Webpack
Vue
Typescript
Linux
软件教程
云服务器
脚本编程
技术扩展
Scoop
SSR
Youtube-dl
You-Get
Typecho
Annie
奇技淫巧
资源分享
Sync Key
随笔
疑难杂症
mysql
Docker
Python
Serverless
登录
Search
标签搜索
docker
K3S
powershell
scoop
webstorm
jQuery
webpack
typecho
mysql
windows10
linux
typescript
ssh
windows11
vue
git
Sync
fastify
winget
github
偏向技术
累计撰写
99
篇文章
累计收到
2
条评论
首页
栏目
前端
CSS
NodeJS
Javascript
小程序
Webpack
Vue
Typescript
Linux
软件教程
云服务器
脚本编程
技术扩展
Scoop
SSR
Youtube-dl
You-Get
Typecho
Annie
奇技淫巧
资源分享
Sync Key
随笔
疑难杂症
mysql
Docker
Python
Serverless
页面
关于
搜索到
28
篇与
前端
的结果
2022-06-10
小程序框架 Taro 和 Uniapp 对比
taro 和 uniapp 均支持跨端小程序开发,本文从编译速度、构建产物、构建时长来对比一下同一项目下的情况
2022年06月10日
688 阅读
0 评论
0 点赞
2021-09-23
优化 Serverless 函数冷启动时间
函数冷启动是使用 serverless 绕不过去的一个坎,无法彻底消除,只能极尽优化,冷启动是指在函数调用链路中包含了代码下载、启动函数实例容器、运行时初始化、用户代码初始化等环节。除平台自有优化外,我们能够做的优化有,代码包尽可能的小,和钱有关的优化有,内存适当加大,使用定时触发器预热函数,使用平台预留资源。 本文通过将 fastify 服务部署到阿里云函数计算来聊聊优化的经历,先说说原先的问题,普通打包后代码约 300M 左右,使用自定义环境,添加了版本较新的环境的包,冷启动耗时平均在 5 ~ 10s,加上 Web 端,得有 20s 左右,这意味着 99% 的用户会在页面渲染完成之前离开网站。 Rollup在新窗口打开 是一个用于 Javascript 的模块打包工具,允许用户将代码和依赖打包成单个文件,输出到不同的类型,应用到不同的环境,更为突出的是 Rollup 的摇树优化(Tree-shaking)功能,由于许多包并未友好处理 package.json 的 files 字段,导致我们安装依赖时会下载许多额外的文件,这时候就需要 tree shaking 进行优化。借助官方和社区众多的插件,我们可以仅依靠配置文件就将项目打包成单个体积极小的文件,相比 Webpack 的庞大复杂,令人摸不着头脑,Rollup 不仅简单易用,容易上手,而且官方文档非常清晰,仅一个页面,再借助 gulp在新窗口打开,即使是有不同的需求,也能轻易实现。 #Rollup 配置 一个简单的应用于开发时的 rollup 配置如下,其含义为,指定入口文件为 src 目录下的 app.ts,输出 commonjs 类型到 outputDir 目录,同时保留模块结构,通过插件 clear在新窗口打开 清空输出目录,copy在新窗口打开 复制相关文件到输出目录,dynamicImportVars在新窗口打开 处理动态模块引入,typescript在新窗口打开 处理 typescript 代码,run在新窗口打开 在构建完毕后会自动运行,watch 监听文件改动。 javascript复制 // rollup.config.js const devConfig = { input: './src/app.ts', output: { dir: outputDir, format: 'cjs', exports: 'named', preserveModules: true, preserveModulesRoot: 'src', }, plugins: [ clear({targets: [outputDir]}), copy({ targets: [{src: './.env.development', dest: outputDir, rename: '.env'}] }), dynamicImportVars({include: ['src/**']}), typescript(), run() ], watch: { chokidar: true, clearScreen: false, } } 123456789101112131415161718192021222324 打包优化主要用于最终编译,根据不同的需求来使用不同的配置,同样的,使用 rollup 不意味着完全无修改,因为 rollup 会对第三方包进行 tree shaking,所以,如果使用的依赖不支持 tree shaking,那么就需要手动进行修改转换,还有一个在使用过程中更常见的问题是循环依赖(circular dependencies),除开可忽略不影响功能的依赖外,其余的就需要我们手动对源码进行删改,这里考究的是对自己项目用到依赖的功能有一个详细的了解,下面贴出生产模式下的 rollup 配置,其中插件 strip在新窗口打开 根据 label 移除相应的块,replace在新窗口打开 替换字符串,json在新窗口打开 转换 json 文件为模块,nodeResolve在新窗口打开 解析 node_modules 模块,commonjs在新窗口打开 转换 CommonJS 代码为 es6 代码,terser在新窗口打开 压缩代码,filesize在新窗口打开 显示包文件大小,通过 rollup 提供的 hook 钩子,实现我们自己的处理。 javascript复制 // rollup.config.js const buildConfig = { input: './src/app.ts', output: { file: outputDir + '/app.js', format: 'cjs', inlineDynamicImports: true, exports: 'named' }, plugins: [ clear({targets: [outputDir]}), copy({ targets: [ {src: ['./node_modules/nodejieba/dict', './node_modules/nodejieba/build/Release/nodejieba.node'], dest: path.join(outputDir, nodejiebaDir)}, ] }), isProd && strip({include: ['**/*.ts', '**/*.js'], labels: ['development']}), json(), replace({ preventAssignment: true, values: { 'require(\'readable-stream/transform\')': 'require(\'stream\').Transform', 'require("readable-stream/transform")': 'require("stream").Transform', 'readable-stream': 'stream' } }), { async transform(code, id) { // mysql2 if (id.match(/node_modules[\\/]mysql2[\\/]index\.js$/)) { code = code.replace('exports.__defineGetter__(\n' + ' \'createConnectionPromise\',\n' + ' () => require(\'./promise.js\').createConnection\n' + ');\n' + '\n' + 'exports.__defineGetter__(\n' + ' \'createPoolPromise\',\n' + ' () => require(\'./promise.js\').createPool\n' + ');\n' + '\n' + 'exports.__defineGetter__(\n' + ' \'createPoolClusterPromise\',\n' + ' () => require(\'./promise.js\').createPoolCluster\n' + ');', '') } if (id.match(/node_modules[\\/]mysql2[\\/]lib[\\/]pool\.js$/)) { code = code.replace('const mysql = require(\'../index.js\');', 'const SqlString = require(\'sqlstring\');'); code = code.replace(/mysql/g, 'SqlString'); code = code.replace(' promise(promiseImpl) {\n' + ' const PromisePool = require(\'../promise\').PromisePool;\n' + ' return new PromisePool(this, promiseImpl);\n' + ' }', '') } if (id.match(/node_modules[\\/]mysql2[\\/]lib[\\/]pool_connection\.js$/)) { code = code.replace('const Connection = require(\'../index.js\').Connection;', 'const Connection = require(\'./connection.js\');'); code = code.replace(' promise(promiseImpl) {\n' + ' const PromisePoolConnection = require(\'../promise\').PromisePoolConnection;\n' + ' return new PromisePoolConnection(this, promiseImpl);\n' + ' }', '') } if (id.match(/node_modules[\\/]mysql2[\\/]lib[\\/]connection\.js$/)) { code = code.replace(' promise(promiseImpl) {\n' + ' const PromiseConnection = require(\'../promise\').PromiseConnection;\n' + ' return new PromiseConnection(this, promiseImpl);\n' + ' }', '') } // nodejieba if (id.match(/node_modules[\\/]nodejieba[\\/]index\.js$/)) { code = code.replace(`var binary = require('@mapbox/node-pre-gyp');`, '') code = code.replace(`var path = require('path');`, '') code = code.replace(`var binding_path = binary.find(path.resolve(path.join(__dirname,'./package.json')));`, '') code = code.replace(`var nodejieba = require(binding_path);`, `var nodejieba = require("./external/nodejieba/nodejieba.node");`) code = code.replace(/\s(dict|hmmDict|userDict|idfDict|stopWordDict)\s/g, ` let $1 `) code = code.replace(/__dirname \+ "(.*?)"/g, `"${nodejiebaDir}$1"`) } return code; } }, dynamicImportVars({include: ['src/**']}), nodeResolve({preferBuiltins: false}), typescript(), commonjs({ignore: id => id.includes('nodejieba.node')}), terser(), filesize(), ] } 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586 使用 rollup 打包之后的源码仅有 500k 不到,加上 nodejieba 模型文件也才 5m 左右,现在冷启动基本保持在 1s 以内 由于 nodejieba 在安装时有区分环境,所以我们需要使用 fun buiild 命令通过 docker 来保证和函数计算环境的一致性,或者从 nodejieba在新窗口打开 官方仓库下载 linux 环境的 nodejieba.node #运行环境修改 修改 fastify 服务运行环境为 nodejs12,当使用 custom 运行环境时,实际容器内 node 版本为 10.x.x,恰好是 nodejieba 不支持的版本,所以我们需要修改运行环境,以 http 触发器为例,提供以下的代理函数,api 网关事件同理 helper/ http.ts typescript复制 export default (app: any, options?: any) => async (req: any, res: any, context: any) => { options = options || {} if (app instanceof Promise) { app = await app } const { method, url, query, payload, headers } = req app.inject({ method, url, query, payload, headers }, (err: any, injectRes: any) => { if (err) { console.error(err) res.setStatusCode(500) res.send('') return } res.setStatusCode(injectRes.statusCode) for (const key in injectRes.headers) { if (injectRes.headers.hasOwnProperty(key)) { const value = injectRes.headers[key] res.setHeader(key, value) } } const contentType = (injectRes.headers['content-type'] || injectRes.headers['Content-Type'] || '').split(';')[0] const isBase64Encoded = options.binaryMimeTypes?.indexOf(contentType) > -1 const body = isBase64Encoded ? injectRes.rawPayload.toString('base64') : injectRes.payload res.send(body) }) } 1234567891011121314151617181920212223242526272829 app.ts typescript复制 import path from 'path' import { fastify, FastifyInstance } from 'fastify' import httpProxy from './helper/http' import root from './routes/root' const init = async () => { const app: FastifyInstance = fastify({ logger: !!process.env.NODE_ENV, connectionTimeout: 20000 }) try { await app.register(root, { prefix: '/' }) return app } catch (err) { app.log.error(err) process.exit(1) } } development: ;(async () => { const app = await init() await app.listen(9000, '0.0.0.0', (err, address) => { if (err) { app.log.error(err) } }) })() module.exports.handler = httpProxy(init()); 123456789101112131415161718192021222324252627282930 #CDN 配置 使用阿里云的 CDN 服务,在函数计算添加自定义域名时,启用 CDN 加速 在 CDN 控制台添加缓存配置 #写作最后 由结果推过程来看,内容并不多,但是真正的经历却是一个不断尝试、发现问题和解决问题的过程 Rollup 值得每位前端开发人员学习和关注!!!
2021年09月23日
208 阅读
0 评论
0 点赞
2021-04-10
mysql://root@localhost/mysql是什么?
# 在使用了fastify-mysql的项目开发中,发现连接数据库时是传的字符串mysql://root@localhost/mysql,和url地址类似,百思不得其解,正常的配置都是有多个选项的一个对象,那么常规理解就是字符串的位置一定对应选项中每个字段,那么,对应关系是如何的呢?源码走起…… 点击直达最后的潘多拉魔盒 #源码分析 fastify-mysql传的参数类型如下,连接类型有两种,连接池和普通连接,默认连接类型是 pool ts复制 // node_modules/fastify-mysql/index.d.ts export type ConnectionType = "connection" | "pool"; export interface MySQLOptions extends PoolOptions, ConnectionOptions { type?: ConnectionType; name?: string; promise?: boolean; connectionString?: string; } 123456789 fastify-mysql底层使用的是mysql2在新窗口打开,从fastify-mysql源码中得知,当 ConnectionType 为 pool 时,调用的是 mysql.createPool 方法,为 connection 时,调用的是 mysql.createConnection 方法 js复制 exports.createPool = function(config) { const PoolConfig = require('./lib/pool_config.js'); return new Pool({ config: new PoolConfig(config) }); }; 1234 再进一步是在 pool_config.js 中处理我们传过来的 connectionstring js复制 === line-highlight data-line: 9-11 === // node_modules/mysql2/lib/pool_config.js const ConnectionConfig = require('./connection_config.js'); class PoolConfig { constructor(options) { if (typeof options === 'string') { options = ConnectionConfig.parseUrl(options); } this.connectionConfig = new ConnectionConfig(options); …… } } module.exports = PoolConfig; 1234567891011121314151617 接着在connection_config.js里面查找,发现了一个 parseUrl 的静态方法,mysql://root@localhost/mysql对应连接数据库配置参数的对应关系就出来了 #对应关系 js复制 static parseUrl(url) { url = urlParse(url, true); const options = { host: url.hostname, port: url.port, database: url.pathname.substr(1) }; if (url.auth) { const auth = url.auth.split(':'); options.user = auth[0]; options.password = auth[1]; } if (url.query) { for (const key in url.query) { const value = url.query[key]; try { // Try to parse this as a JSON expression first options[key] = JSON.parse(value); } catch (err) { // Otherwise assume it is a plain string options[key] = value; } } } return options; } 1234567891011121314151617181920212223242526 #URL额外补充 url是统一资源定位符(Uniform Resource Locator)的缩写,他的格式为: markdown复制 [comment]: <> (标准格式) [协议类型]://[服务器地址]:[端口号]/[资源层级UNIX文件路径][文件名]?[查询]#[片段ID] [comment]: <> (完整格式) [协议类型]://[访问资源需要的凭证信息]@[服务器地址]:[端口号]/[资源层级UNIX文件路径][文件名]?[查询]#[片段ID] [comment]: <> (其中[访问凭证信息]、[端口号]、[查询]、[片段ID]属于选填项) 1234567 统一资源定位符在新窗口打开不但被用作网页地址,JDBC在新窗口打开 客户端在新窗口打开也使用统一资源定位符连接其数据库服务器。作为对比,ODBC在新窗口打开 的连接字符串作用相同,但并不采用 URL 格式,而是分号和等号分隔的键值对。右侧是一个 Oracle在新窗口打开 数据库的统一资源定位符:jdbc:datadirect:oracle://myserver:1521;sid=testdb #最终呈现 假设有这样一个地址:mysql://user:password@host:3306/database?charset=utf8,Node环境解析如下 注意,传统的urlObject方式(即以下方式)在v11.0.0开始弃用,新的方式为new URL(<URL>) js复制 const urlParse = require('url').parse; urlParse('mysql://user:password@host:3306/database?charset=utf8') Url { protocol: 'mysql:', slashes: true, auth: 'user:password', host: 'host:3306', port: '3306', hostname: 'host', hash: null, search: '?charset=utf8', query: 'charset=utf8', pathname: '/database', path: '/database?charset=utf8', href: 'mysql://user:password@host:3306/database?charset=utf8' } // 通过上面的parseUrl解析之后得到 { host: 'host', port: '3306', database: 'database', user: 'user', password: 'password', charset: 'utf8' } 123456789101112131415161718192021222324252627 #总结 所以,我这一顿操作分析了个寂寞??????
2021年04月10日
195 阅读
0 评论
0 点赞
2021-03-26
Vue3.x学习笔记
记录一下学习Vue3.x的过程 #基本格式 javascript复制 const app = Vue.createApp({ /* TODO */ }) 123 #计算属性缓存 VS 方法 计算属性是基于它们的反应依赖关系缓存的,即计算属性中的变量改变才改变,否则多次使用仍旧是上一次的结果。 #事件 模板中包含一个特殊变量$event,同样可以有多个方法,比如: html复制 <button @click="one($event), two($event)"></button> 1 原来2.x版本是是通过.native修饰符来指明一个原生事件,现在通过 emits 选项在组件上定义已发出的事件,皆为emit事件,emit事件可增加验证,比如: js复制 app.component('custom-form', { emits: { // 没有验证 click: null, // 验证submit 事件 submit: ({ email, password }) => { if (email && password) { return true } else { console.warn('Invalid submit event payload!') return false } } }, methods: { submitForm() { this.$emit('submit', { email, password }) } } }) 123456789101112131415161718192021 v-model的语法糖与2.x版本也有所不同,2.x是propvalue和事件input,3.x版本是propmodelValue和事件update:modelValue,如果要修改value名称,可以通过向v-model传递参数,比如: html复制 <my-component v-model:title="bookTitle"></my-component> 1 子组件修改如下 js复制 app.component('my-component', { props: { title: String }, emits: ['update:title'], template: ` <input type="text" :value="title" @input="$emit('update:title', $event.target.value)"> ` }) 123456789101112 利用此特性,通过传递参数,增加多个v-model双向绑定 关于修饰符,3.x版本提供了一个modelModifiers属性,比如默认的,假设有一个v-model.capitalize="myText",那么子组件内的props属性中将会有modelModifiers字段,字段值为{ capitalize: true }。对于带参数的v-model绑定,子组件内的props属性中将会有arg + "Modifiers",比如,v-model:description.capitalize="myText",子组件的props属性上将会有descriptionModifiers字段 #组件 可通过inheritAttrs: false来禁用组件属性和事件继承,继承指的是:在模板中引入组件上的属性和事件,会被组件内的根节点继承,比如: html复制 <div id="date-picker" class="demo"> <date-picker @change="showChange" data-status="activated"></date-picker> </div <!-- date-picker组件,会继承父级绑定的属性和事件 --> app.component('date-picker', { template: ` <select> <option value="1">Yesterday</option> <option value="2">Today</option> <option value="3">Tomorrow</option> </select> ` }) 1234567891011121314 当禁用继承后,我们可以将组件的$attrs属性赋给我们期待的节点上,$attrs属性包括组件 props 和 emits property 中未包含的所有属性 (例如,class、style、v-on 监听器等)。比如: js复制 app.component('date-picker', { inheritAttrs: false, template: ` <div class="date-picker"> <input type="datetime" v-bind="$attrs" /> </div> ` }) 12345678 多个根节点需显示指定,vue2.x不支持多个根节点 html复制 <custom-layout id="custom-layout" @click="changeValue"></custom-layout> 1 js复制 // 这将发出警告 app.component('custom-layout', { template: ` <header>...</header> <main>...</main> <footer>...</footer> ` }) // 没有警告,$attrs被传递到<main>元素 app.component('custom-layout', { template: ` <header>...</header> <main v-bind="$attrs">...</main> <footer>...</footer> ` }) 1234567891011121314151617 #插槽slot 插槽缩写:# 2.x版本通过slot="<slot_name>"声明使用一个具名插槽,3.x版本通过v-slot:<slot_name>声明使用 2.x版本通过slot-scope="<attribute>"来绑定属性,3.x版本通过v-slot:<slot_name>="<attribute>"来绑定属性 需要注意的是,v-slot和v-slot:<slot_name>不能混用,容易导致作用域不明确 支持动态插槽名,v-slot:[dynamicSlotName] #Provide / Inject 跨祖孙组件传递数据时很有用,假设有一个组件继承关系如下,A组件包含B组件,B组件包含C组件,如果有需求需要A组件传值给C组件,可通过以下方式 js复制 // A组件 provide: { user: 'John Doe' } // C组件 inject: ['user'] 1234567 需要注意的是,如果provide了组件实例上的属性,将会抛出异常,但是,如果provide返回了对象的函数,将会可以正常传递 js复制 app.component('todo-list', { data() { return { todos: ['Feed a cat', 'Buy tickets'] } }, provide: { todoLength: this.todos.length // 将会导致错误 `Cannot read property 'length' of undefined` }, template: ` ... ` }) 12345678910111213 js复制 app.component('todo-list', { data() { return { todos: ['Feed a cat', 'Buy tickets'] } }, provide() { return { todoLength: this.todos.length // 正常传递 } }, template: ` ... ` }) 123456789101112131415 默认情况下,传递不是响应式的,即todoLength改变,inject接收到的值不会跟着改变,可以通过传递一个 ref property 或 reactive 对象给 provide 来改变这种行为。这里通过分配一个组合式 API computed 来保持响应式。 js复制 provide() { return { todoLength: Vue.computed(() => this.todos.length) } } 12345 #组合式API setup函数接收props和context参数,setup返回的内容将暴露给组件其余部分,还可以返回一个渲染函数 context有三个参数,attrs,slots,emit, ref接收参数,内部将其响应式,并返回一个带有value属性的对象,因为对象是引用传递,而Number和String是值传递 toRefs,响应式对象不能使用ES6解构,因为它会消除响应性,通过toRefs可以实现ES6解构,比如props toRef可以给响应式对象添加一个响应式的属性,比如const title = toRef(props, 'title') reactive为JavaScript 对象创建响应式状态 Ref 展开仅发生在被响应式 Object 嵌套的时候。当从 Array 或原生集合类型如 Map访问 ref 时,不会进行展开: setup中也可以使用生命周期钩子,结构前缀为 on,比如onMounted watch响应式更改,接收3个参数 一个响应式引用或getter函数 一个回调 可选的配置选项 执行setup时,组件实例尚未被创建,因此只能访问props,attrs,slots,emit,无法访问data,computed,methods #自定义指令 钩子函数相比2.x版本有所变化 created:在绑定元素的 attribute 或事件监听器被应用之前调用,在指令需要附加须要在普通的 v-on 事件监听器前调用的事件监听器时,这很有用。 beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用。 mounted:在绑定元素的父组件被挂载后调用。 beforeUpdate:在更新包含组件的 VNode 之前调用。 updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用。 beforeUnmount:在卸载绑定元素的父组件之前调用 unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次。 #teleport 可以自定义挂载到的元素,比如<teleport to="body"> #渲染函数 js复制 // @returns {VNode} h( // {String | Object | Function | null} tag // 一个 HTML 标签名、一个组件、一个异步组件,或者 null。 // 使用 null 将会渲染一个注释。 // // 必需的。 'div', // {Object} props // 与 attribute、prop 和事件相对应的对象。 // 我们会在模板中使用。 // // 可选的。 {}, // {String | Array | Object} children // 子 VNodes, 使用 `h()` 构建, // 或使用字符串获取 "文本 Vnode" 或者 // 有插槽的对象。 // // 可选的。 [ 'Some text comes first.', h('h1', 'A headline'), h(MyComponent, { someProp: 'foobar' }) ] ) 123456789101112131415161718192021222324252627282930 #Proxy Proxy 是一个包含另一个对象或函数并允许你对其进行拦截的对象。使用语法new Proxy(target, handler)
2021年03月26日
259 阅读
0 评论
0 点赞
2021-01-13
Webpack配置参数
#入口(entry) 入口起点,默认值是./src/index.js,可以设置为一个或多个入口起点。 #单个入口(简写)语法 用法:entry: string | [string] javascript复制 // 单入口(简写)语法 module.exports = { entry: './src/index.js' }; module.exports = { entry: { main: './src/index.js' } }; module.exports = { entry: [ './src/index.js', './src/app.js' ], output: { filename: 'bundle.js' } }; 123456789101112131415161718 #对象语法 用法:entry: { <entryChunkName> string | [string] } | {} 对象语法会比较繁琐。然而,这是应用程序中定义入口的最可扩展的方式。 javascript复制 module.exports = { entry: { app: './src/app.js', adminApp: './src/adminApp.js' } }; 123456 #常见场景 分离 app(应用程序) 和 vendor(第三方库) 入口 下面示例是告诉 webpack 我们想要配置 2 个单独的入口点,这样你就可以在 vendor.js 中存入未做修改的必要 library 或文件(例如 Bootstrap, jQuery, 图片等),然后将它们打包在一起成为单独的 chunk。内容哈希保持不变,这使浏览器可以独立地缓存它们,从而减少了加载时间。 javascript复制 // webpack.config.js module.exports = { entry: { main: './src/app.js', vendor: './src/vendor.js' } }; // webpack.prod.js module.exports = { output: { filename: '[name].[contenthash].bundle.js' } }; // webpack.dev.js module.exports = { output: { filename: '[name].bundle.js' } }; 12345678910111213141516171819 多页面应用程序 下面示例是告诉 webpack 需要三个独立分离的依赖图,在多页面应用程序中,server 会拉取一个新的 HTML 文档给你的客户端。页面重新加载此新文档,并且资源被重新下载。然而,这给了我们特殊的机会去做很多事,例如使用 `optimization.splitChunks`在新窗口打开 为页面间共享的应用程序代码创建 bundle。由于入口起点数量的增多,多页应用能够复用多个入口起点之间的大量代码/模块,从而可以极大地从这些技术中受益。 javascript复制 // webpack.config.js module.exports = { entry: { pageOne: './src/pageOne/index.js', pageTwo: './src/pageTwo/index.js', pageThree: './src/pageThree/index.js' } }; 12345678 #输出(output) 可以通过配置 output 选项,告知 webpack 如何向硬盘写入编译文件。注意,即使可以存在多个 entry 起点,但只能指定一个 output 配置。 #用法 在 webpack 配置中,output 属性的最低要求是,将它的值设置为一个对象,然后为将输出文件的文件名配置为一个 `output.filename`在新窗口打开: 以下配置将一个单独的 bundle.js 文件输出到 dist 目录中。 javascript复制 // webpack.config.js module.exports = { output: { filename: 'bundle.js', } }; 123456 #多个入口起点 如果配置中创建出多于一个 "chunk"(例如,使用多个入口起点或使用像 CommonsChunkPlugin 这样的插件),则应该使用 占位符(substitutions)在新窗口打开 来确保每个文件具有唯一的名称。 javascript复制 module.exports = { entry: { app: './src/app.js', search: './src/search.js' }, output: { filename: '[name].js', path: __dirname + '/dist' } }; // 写入到硬盘:./dist/app.js, ./dist/search.js 123456789101112 #高级进阶 以下是对资源使用 CDN 和 hash 的复杂示例: javascript复制 // config.js module.exports = { //... output: { path: '/home/proj/cdn/assets/[fullhash]', publicPath: 'https://cdn.example.com/assets/[fullhash]/' } }; 12345678 如果在编译时,不知道最终输出文件的 publicPath 是什么地址,则可以将其留空,并且在运行时通过入口起点文件中的 __webpack_public_path__ 动态设置。 javascript复制 __webpack_public_path__ = myRuntimePublicPath; // 应用程序入口的其余部分 123 #loader loader 用于对模块的源代码进行转换。loader 可以使你在 import 或 "load(加载)" 模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件! #示例 例如,你可以使用 loader 告诉 webpack 加载 CSS 文件,或者将 TypeScript 转为 JavaScript。为此,首先安装相对应的 loader: bash复制 npm install --save-dev css-loader ts-loader 1 然后指示 webpack 对每个 .css 使用 `css-loader`在新窗口打开,以及对所有 .ts 文件使用 `ts-loader`在新窗口打开: js复制 // webpack.config.js module.exports = { module: { rules: [ { test: /\.css$/, use: 'css-loader' }, { test: /\.ts$/, use: 'ts-loader' } ] } }; 123456789 #使用 loader 在你的应用程序中,有三种使用 loader 的方式: 配置方式在新窗口打开(推荐):在 webpack.config.js 文件中指定 loader。 内联方式在新窗口打开:在每个 import 语句中显式指定 loader CLI 方式在新窗口打开:在 shell 命令中指定它们。 #配置方式 `module.rules`在新窗口打开 允许你在 webpack 配置中指定多个 loader。 这种方式是展示 loader 的一种简明方式,并且有助于使代码变得简洁和易于维护。同时让你对各个 loader 有个全局概览: loader 从右到左(或从下到上)地取值(evaluate)/执行(execute)。在下面的示例中,从 sass-loader 开始执行,然后继续执行 css-loader,最后以 style-loader 为结束。查看 loader 功能在新窗口打开 章节,了解有关 loader 顺序的更多信息。 js复制 module.exports = { module: { rules: [ { test: /\.css$/, use: [ // [style-loader](/loaders/style-loader) { loader: 'style-loader' }, // [css-loader](/loaders/css-loader) { loader: 'css-loader', options: { modules: true } }, // [sass-loader](/loaders/sass-loader) { loader: 'sass-loader' } ] } ] } }; 12345678910111213141516171819202122 #内联方式 可以在 import 语句或任何 与 "import" 方法同等的引用方式在新窗口打开 中指定 loader。使用 ! 将资源中的 loader 分开。每个部分都会相对于当前目录解析。 vue复制 import Styles from 'style-loader!css-loader?modules!./styles.css'; 1 通过为内联 import 语句添加前缀,可以覆盖 配置在新窗口打开 中的所有 loader, preLoader 和 postLoader: 使用 ! 前缀,将禁用所有已配置的 normal loader(普通 loader) vue复制 import Styles from '!style-loader!css-loader?modules!./styles.css'; 1 使用 !! 前缀,将禁用所有已配置的 loader(preLoader, loader, postLoader) vue复制 import Styles from '!!style-loader!css-loader?modules!./styles.css'; 1 使用 -! 前缀,将禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoaders vue复制 import Styles from '-!style-loader!css-loader?modules!./styles.css'; 1 选项可以传递查询参数,例如 ?key=value&foo=bar,或者一个 JSON 对象,例如 ?{"key":"value","foo":"bar"}。 尽可能使用 module.rules,因为这样可以减少源码中样板文件的代码量,并且可以在出错时,更快地调试和定位 loader 中的问题。 #CLI 方式 还可以通过 CLI 使用 loader: shell复制 webpack --module-bind pug-loader --module-bind 'css=style-loader!css-loader' 1 这会对 .jade 文件使用 pug-loader,以及对 .css 文件使用 `style-loader`在新窗口打开 和 `css-loader`在新窗口打开。 #loader 特性 loader 支持链式调用。链中的每个 loader 会将转换应用在已处理过的资源上。一组链式的 loader 将按照相反的顺序执行。链中的第一个 loader 将其结果(也就是应用过转换后的资源)传递给下一个 loader,依此类推。最后,链中的最后一个 loader,返回 webpack 所期望的 JavaScript。 loader 可以是同步的,也可以是异步的。 loader 运行在 Node.js 中,并且能够执行任何操作。 loader 可以通过 options 对象配置(仍然支持使用 query 参数来设置选项,但是这种方式已被废弃)。 除了常见的通过 package.json 的 main 来将一个 npm 模块导出为 loader,还可以在 module.rules 中使用 loader 字段直接引用一个模块。 插件(plugin)可以为 loader 带来更多特性。 loader 能够产生额外的任意文件。 可以通过 loader 的预处理函数,为 JavaScript 生态系统提供更多能力。用户现在可以更加灵活地引入细粒度逻辑,例如:压缩、打包、语言翻译和 更多其他特性在新窗口打开。 #解析 loader loader 遵循标准 模块解析在新窗口打开 规则。多数情况下,loader 将从 模块路径在新窗口打开 加载(通常是从 npm install, node_modules 进行加载)。 我们预期 loader 模块导出为一个函数,并且编写为 Node.js 兼容的 JavaScript。通常使用 npm 进行管理 loader,但是也可以将应用程序中的文件作为自定义 loader。按照约定,loader 通常被命名为 xxx-loader(例如 json-loader)。更多详细信息,请查看 编写一个 loader在新窗口打开。 #plugin 插件是 webpack 的 支柱在新窗口打开 功能。webpack 自身也是构建于你在 webpack 配置中用到的相同的插件系统之上! 插件目的在于解决 loader在新窗口打开 无法实现的其他事。 如果在插件中使用了 `webpack-sources`在新窗口打开 的 package,请使用 require('webpack').sources 替代 require('webpack-sources'),以避免持久缓存的版本冲突。 #剖析 webpack 插件是一个具有 `apply`在新窗口打开 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象。 javascript复制 // ConsoleLogOnBuildWebpackPlugin.js const pluginName = 'ConsoleLogOnBuildWebpackPlugin'; class ConsoleLogOnBuildWebpackPlugin { apply(compiler) { compiler.hooks.run.tap(pluginName, compilation => { console.log('webpack 构建过程开始!'); }); } } module.exports = ConsoleLogOnBuildWebpackPlugin; 123456789101112 compiler hook 的 tap 方法的第一个参数,应该是驼峰式命名的插件名称。建议为此使用一个常量,以便它可以在所有 hook 中重复使用。 #用法 由于插件可以携带参数/选项,你必须在 webpack 配置中,向 plugins 属性传入一个 new 实例。 取决于你的 webpack 用法,对应有多种使用插件的方式。 #配置方式 javascript复制 // webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装 const webpack = require('webpack'); // 访问内置的插件 const path = require('path'); module.exports = { entry: './path/to/my/entry/file.js', output: { filename: 'my-first-webpack.bundle.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.(js|jsx)$/, use: 'babel-loader' } ] }, plugins: [ new webpack.ProgressPlugin(), new HtmlWebpackPlugin({template: './src/index.html'}) ] }; 123456789101112131415161718192021222324 ProgressPlugin 用于自定义编译过程中的进度报告,HtmlWebpackPlugin 将生成一个 HTML 文件,并在其中使用 script 引入一个名为 my-first-webpack.bundle.js 的 JS 文件。 #Node API 方式 在使用 Node API 时,还可以通过配置中的 plugins 属性传入插件。 javascript复制 // some-node-script.js const webpack = require('webpack'); // 访问 webpack 运行时(runtime) const configuration = require('./webpack.config.js'); let compiler = webpack(configuration); new webpack.ProgressPlugin().apply(compiler); compiler.run(function(err, stats) { // ... }); 1234567891011 #配置(Configuration) 你可能已经注意到,很少有 webpack 配置看起来完全相同。这是因为 webpack 的配置文件是 JavaScript 文件,文件内导出了一个 webpack 配置的对象在新窗口打开。 webpack 会根据该配置定义的属性进行处理。 由于 webpack 遵循 CommonJS 模块规范,因此,你可以在配置中使用: 通过 require(...) 引入其他文件 通过 require(...) 使用 npm 下载的工具函数 使用 JavaScript 控制流表达式,例如 ?: 操作符 对 value 使用常量或变量赋值 编写并执行函数,生成部分配置 请在合适的场景,使用这些功能。 虽然技术上可行,但还是应避免如下操作: 当使用 webpack CLI 工具时,访问 CLI 参数(应编写自己的 CLI 工具替代,或者使用 `--env`在新窗口打开) 导出不确定的结果(两次调用 webpack 应产生相同的输出文件) 编写超长的配置(应将配置文件拆分成多个) 此文档中得出最重要的结论是,webpack 的配置可以有许多不同的样式和风格。关键在于,为了易于维护和理解这些配置,需要在团队内部保证一致。 接下来的示例中,展示了 webpack 配置如何实现既可表达,又可灵活配置,这主要得益于配置即为代码: #基本配置 查看:配置章节在新窗口打开中所有支持的配置选项。 javascript复制 // webpack.config.js var path = require('path'); module.exports = { mode: 'development', entry: './foo.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'foo.bundle.js' } }; 1234567891011 #多个 target 除了可以将单个配置导出为 object,function在新窗口打开 或 Promise在新窗口打开 以外,还可以将其导出为多个配置。 查看:导出多个配置在新窗口打开 #使用其它配置语言 webpack 支持由多种编程和数据语言编写的配置文件。 查看:配置语言在新窗口打开 #模块(Modules) 在模块化编程在新窗口打开中,开发者将程序分解为功能离散的 chunk,并称之为 模块。 每个模块都拥有小于完整程序的体积,使得验证、调试及测试变得轻而易举。 精心编写的 模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具备了条理清晰的设计和明确的目的。 Node.js 从一开始就支持模块化编程。 然而,web 的_模块化_正在缓慢支持中。 在 web 界存在多种支持 JavaScript 模块化的工具,这些工具各有优势和限制。 webpack 从这些系统中汲取了经验和教训,并将_模块_的概念应用到项目的任何文件中。 #何为 webpack 模块 与 Node.js 模块在新窗口打开相比,webpack _模块_能以各种方式表达它们的依赖关系。下面是一些示例: ES2015 `import`在新窗口打开 语句 CommonJS在新窗口打开 require() 语句 AMD在新窗口打开 define 和 require 语句 css/sass/less 文件中的 `@import` 语句在新窗口打开。 stylesheet url(...) 或者 HTML <img src=...> 文件中的图片链接。 #支持的模块类型 webpack 天生支持如下模块类型: ECMAScript 模块在新窗口打开 CommonJS 模块 AMD 模块 Assets在新窗口打开 WebAssembly 模块 通过 loader 可以使 webpack 支持多种语言和预处理器语法编写的模块。loader 向 webpack 描述了如何处理非原生模块,并将相关依赖引入到你的 bundles中。 webpack 社区已经为各种流行的语言和预处理器创建了 loader,其中包括: CoffeeScript在新窗口打开 TypeScript在新窗口打开 ESNext (Babel)在新窗口打开 Sass在新窗口打开 Less在新窗口打开 Stylus在新窗口打开 Elm在新窗口打开 当然还有更多!总得来说,webpack 提供了可定制,强大且丰富的 API,允许在 任何技术栈 中使用,同时支持在开发、测试和生产环境的工作流中做到 无侵入性。 关于 loader 的相关信息,请参考 **loader 列表**在新窗口打开 或 **自定义 loader**在新窗口打开。 #模块解析(Module Resolution) resolver 是一个帮助寻找模块绝对路径的库。 一个模块可以作为另一个模块的依赖模块,然后被后者引用,如下: js复制 import foo from 'path/to/module'; // 或者 require('path/to/module'); 123 所依赖的模块可以是来自应用程序的代码或第三方库。 resolver 帮助 webpack 从每个 require/import 语句中,找到需要引入到 bundle 中的模块代码。 当打包模块时,webpack 使用 enhanced-resolve在新窗口打开 来解析文件路径。 #webpack 中的解析规则 使用 enhanced-resolve,webpack 能解析三种文件路径 #绝对路径 js复制 import '/home/me/file'; import 'C:\\Users\\me\\file'; 123 由于已经获得文件的绝对路径,因此不需要再做进一步解析。 #相对路径 js复制 import '../src/file1'; import './file2'; 12 在这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录。在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径。 #模块路径 js复制 import 'module'; import 'module/lib/file'; 12 在 `resolve.modules`在新窗口打开 中指定的所有目录检索模块。 你可以通过配置别名的方式来替换初始模块路径,具体请参照 `resolve.alias`在新窗口打开 配置选项。 如果 package 中包含 package.json 文件,那么在 `resolve.exportsFields`在新窗口打开 配置选项中指定的字段会被依次查找,package.json 中的第一个字段会根据 package 导出指南在新窗口打开确定 package 中可用的 export。 一旦根据上述规则解析路径后,resolver 将会检查路径是指向文件还是文件夹。如果路径指向文件: 如果文件具有扩展名,则直接将文件打包。 否则,将使用 `resolve.extensions`在新窗口打开 选项作为文件扩展名来解析,此选项会告诉解析器在解析中能够接受那些扩展名(例如 .js,.jsx)。 如果路径指向一个文件夹,则进行如下步骤寻找具有正确扩展名的文件: 如果文件夹中包含 package.json 文件,则会根据 `resolve.mainFields`在新窗口打开 配置中的字段顺序查找,并根据 package.json 中的符合配置要求的第一个字段来确定文件路径。 如果不存在 package.json 文件或 `resolve.mainFields`在新窗口打开 没有返回有效路径,则会根据 `resolve.mainFiles`在新窗口打开 配置选项中指定的文件名顺序查找,看是否能在 import/require 的目录下匹配到一个存在的文件名。 然后使用 `resolve.extensions`在新窗口打开 选项,以类似的方式解析文件扩展名。 webpack 会根据构建目标,为这些选项提供合理的默认在新窗口打开配置。 #解析 loader loader 的解析规则也遵循特定的规范。但是 `resolveLoader`在新窗口打开 配置项可以为 loader 设置独立的解析规则。 #缓存 每次文件系统访问文件都会被缓存,以便于更快触发对同一文件的多个并行或串行请求。在 watch 模式在新窗口打开 下,只有修改过的文件会被从缓存中移出。如果关闭 watch 模式,则会在每次编译前清理缓存。 欲了解更多上述配置信息,请查阅 Resolve API在新窗口打开。 #Module Federation #动机 多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。 这通常被称作微前端,但并不仅限于此。 #底层概念 我们区分本地模块和远程模块。本地模块即为普通模块,是当前构建的一部分。远程模块不属于当前构建,并在运行时从所谓的容器加载。 加载远程模块被认为是异步操作。当使用远程模块时,这些异步操作将被放置在远程模块和入口之间的下一个 chunk 的加载操作中。如果没有 chunk 加载操作,就不能使用远程模块。 chunk 的加载操作通常是通过调用 import() 实现的,但也支持像 require.ensure 或 require([...]) 之类的旧语法。 容器是由容器入口创建的,该入口暴露了对特定模块的异步访问。暴露的访问分为两个步骤: 加载模块(异步的) 执行模块(同步的) 步骤 1 将在 chunk 加载期间完成。步骤 2 将在与其他(本地和远程)的模块交错执行期间完成。这样一来,执行顺序不受模块从本地转换为远程或从远程转为本地的影响。 容器可以嵌套使用,容器可以使用来自其他容器的模块。容器之间也可以循环依赖。 #重载(Overriding) 容器能够将选定的本地模块标记为“可重载”。容器的使用者能够提供“重载”,即替换容器中的一个“可重载”的模块。当使用者提供重载模块时,容器的所有模块将使用替换模块而非本地模块。当使用者不提供替换模块时,容器的所有模块将使用本地模块。 容器管理可重载模块的方式为:当使用者已经重写它们后,就不需要下载了。这通常是通过将它们放在单独的 chunk 中来实现的。 另一方面,替换模块的提供者,将只提供异步加载函数。它允许容器仅在需要替换模块时才去加载。提供者管理替换模块的方式为:当容器不请求替换模块时,则无需下载。这通常是通过将它们放在单独的 chunk 中来实现的。 "name" 用于标识容器中可重载的模块。 重载(Overrides)的提供和容器暴露模块类似,它分为两个步骤: 加载(异步) 执行(同步) 当嵌套使用时,向容器提供重载将自动覆盖嵌套容器中具有相同 "name" 的模块。 必须在容器模块加载之前提供重载。在初始 chunk 中使用的重载只能被不使用 Promise 的同步模块重载。一旦执行,就不可再次被重载。 #高级概念 每个构建都充当一个容器,也可将其他构建作为容器。通过这种方式,每个构建都能够通过从对应容器中加载模块来访问其他容器暴露出来的模块。 共享模块是指既可重写的又可作为向嵌套容器提供重写的模块。它们通常指向每个构建中的相同模块,例如相同的库。 packageName 选项允许通过设置包名来查找所需的版本。默认情况下,它会自动推断模块请求,当想禁用自动推断时,请将 requiredVersion 设置为 false 。 #构建块(Building blocks) #OverridablesPlugin (底层 API) 这个插件使得特定模块“可重载”。一个本地 API ( __webpack_override__ ) 允许提供重载。 javascript复制 // webpack.config.js const OverridablesPlugin = require('webpack/lib/container/OverridablesPlugin'); module.exports = { plugins: [ new OverridablesPlugin([ { // 通过 OverridablesPlugin 定义一个可重载的模块 test1: './src/test1.js', }, ]), ], }; 123456789101112 javascript复制 // src/index.js __webpack_override__({ // 这里我们重写 test1 模块 test1: () => 'I will override test1 module under src', }); 12345 #ContainerPlugin (底层 API) 该插件使用指定的公开模块来创建一个额外的容器入口。它还会在内部使用 OverridablesPlugin,并向容器的使用者暴露 override API。 #ContainerReferencePlugin (底层 API) 该插件将特定的引用添加到作为外部资源(externals)的容器中,并允许从这些容器中导入远程模块。它还会调用这些容器的 override API 来为它们提供重载。本地的重载(当构建也是一个容器时,通过 __webpack_override__ 或 override API)和指定的重载被提供给所有引用的容器。 #ModuleFederationPlugin (高级 API) 该插件组合了 ContainerPlugin 和 ContainerReferencePlugin。重载(overrides)和可重载(overridables)被合并到指定共享模块的单个列表中。 #概念目标 它既可以暴露,又可以使用 webpack 支持的任何模块类型 代码块加载应该并行加载所需的所有内容(web:到服务器的单次往返) 从使用者到容器的控制 重写模块是一种单向操作 同级容器不能重写彼此的模块。 概念适用于独立于环境 可用于 web、Node.js 等 共享中的相对和绝对请求 会一直提供,即使不使用 会将相对路径解析到 config.context 默认不会使用 requiredVersion 共享中的模块请求 只在使用时提供 会匹配构建中所有使用的相等模块请求 将提供所有匹配模块 将从图中这个位置的 package.json 提取 requiredVersion 当你有嵌套的 node_modules 时,可以提供和使用多个不同的版本 共享中尾部带有 / 的模块请求将匹配所有具有这个前缀的模块请求 #用例 #每个页面单独构建 单页应用的每个页面都是在单独的构建中从容器暴露出来的。主体应用程序(application shell)也是独立构建,会将所有页面作为远程模块来引用。通过这种方式,可以单独部署每个页面。在更新路由或添加新路由时部署主体应用程序。主体应用程序将常用库定义为共享模块,以避免在页面构建中出现重复。 #将组件库作为容器 许多应用程序共享一个通用的组件库,可以将其构建成暴露所有组件的容器。每个应用程序使用来自组件库容器的组件。可以单独部署对组件库的更改,而不需要重新部署所有应用程序。应用程序自动使用组件库的最新版本。 #动态远程容器 该容器接口支持 get 和 init 方法。 init 是一个兼容 async 的方法,调用时,只含有一个参数:共享作用域对象(shared scope object)。此对象在远程容器中用作共享作用域,并由 host 提供的模块填充。 可以利用它在运行时动态地将远程容器连接到 host 容器。 javascript复制 // init.js (async () => { // 初始化共享作用域(shared scope)用提供的已知此构建和所有远程的模块填充它 await __webpack_init_sharing__('default'); const container = window.someContainer; // 或从其他地方获取容器 // 初始化容器 它可能提供共享模块 await container.init(__webpack_share_scopes__.default); const module = await container.get('./module'); })(); 123456789 容器尝试提供共享模块,但是如果共享模块已经被使用,则会发出警告,并忽略所提供的共享模块。容器仍能将其作为降级模块。 你可以通过动态加载的方式,提供一个共享模块的不同版本,从而实现 A/B 测试。 在尝试动态连接远程容器之前,确保已加载容器。 例子: javascript复制 // init.js function loadComponent(scope, module) { return async () => { // 初始化共享作用域(shared scope)用提供的已知此构建和所有远程的模块填充它 await __webpack_init_sharing__('default'); const container = window[scope]; // 或从其他地方获取容器 // 初始化容器 它可能提供共享模块 await container.init(__webpack_share_scopes__.default); const factory = await window[scope].get(module); const Module = factory(); return Module; }; } loadComponent('abtests', 'test123'); 123456789101112131415 查看完整实现在新窗口打开 #故障排除 #Uncaught Error: Shared module is not available for eager consumption 应用程序正急切地执行一个作为全局主机运行的应用程序。有如下选项可供选择: 你可以在模块联邦的高级 API 中将依赖设置为即时依赖,此 API 不会将模块放在异步 chunk 中,而是同步地提供它们。这使得我们在初始块中可以直接使用这些共享模块。但是要注意,由于所有提供的和降级模块是要异步下载的,因此,建议只在应用程序的某个地方提供它,例如 shell。 我们强烈建议使用异步边界(asynchronous boundary)。它将把初始化代码分割成更大的块,以避免任何额外的开销,以提高总体性能。 例如,你的入口看起来是这样的: javascript复制 // index.js import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render(<App />, document.getElementById('root')); 12345 让我们创建 bootstrap.js 文件,并将入口文件的内容放到里面,然后将 bootstrap 引入到入口文件中: index.js diff复制 + import('./bootstrap'); - import React from 'react'; - import ReactDOM from 'react-dom'; - import App from './App'; - ReactDOM.render(<App />, document.getElementById('root')); 12345 bootstrap.js diff复制 + import React from 'react'; + import ReactDOM from 'react-dom'; + import App from './App'; + ReactDOM.render(<App />, document.getElementById('root')); 1234 这种方法有效,但存在局限性或缺点。 通过 ModuleFederationPlugin 将依赖的 eager 属性设置为 true webpack.config.js javascript复制 // ... new ModuleFederationPlugin({ shared: { ...deps, react: { eager: true, } } }); 123456789 #Uncaught Error: Module "./Button" does not exist in container. 错误提示中可能不会显示 "./Button",但是信息看起来差不多。这个问题通常会出现在将 webpack beta.16 升级到 webpack beta.17 中。 在 ModuleFederationPlugin 里,更改 exposes: diff复制 new ModuleFederationPlugin({ exposes: { - 'Button': './src/Button' + './Button':'./src/Button' } }); 123456 #Uncaught TypeError: fn is not a function 此处错误可能是丢失了远程容器,请确保在使用前添加它。 如果已为试图使用远程服务器的容器加载了容器,但仍然看到此错误,则需将主机容器的远程容器文件也添加到 HTML 中。 #依赖图(dependency graph) 每当一个文件依赖另一个文件时,webpack 都会将文件视为直接存在 依赖关系。这使得 webpack 可以获取非代码资源,如 images 或 web 字体等。并会把它们作为 依赖 提供给应用程序。 当 webpack 处理应用程序时,它会根据命令行参数中或配置文件中定义的模块列表开始处理。 从 *入口*在新窗口打开 开始,webpack 会递归的构建一个依赖关系图,这个依赖图包含着应用程序中所需的每个模块,然后将所有模块打包为少量的 bundle —— 通常只有一个 —— 可由浏览器加载。 对于 HTTP/1.1 的应用程序来说,由 webpack 构建的 bundle 非常强大。当浏览器发起请求时,它能最大程度的减少应用的等待时间。而对于 HTTP/2 来说,你还可以使用代码分割在新窗口打开进行进一步优化。 #target 由于 JavaScript 即可以编写服务端代码也可以编写浏览器代码,所以 webpack 提供了多种部署 target,你可以在 webpack 的配置选项在新窗口打开中进行设置。 webpack 的 target 属性,不要和 output.libraryTarget 属性混淆。有关 output 属性的更多信息,请参阅 output 指南在新窗口打开 #用法 想设置 target 属性,只需在 webpack 配置中设置 target 字段: webpack.config.js javascript复制 module.exports = { target: 'node' }; 123 在上述示例中,target 设置为 node,webpack 将在类 Node.js 环境编译代码。(使用 Node.js 的 require 加载 chunk,而不加载任何内置模块,如 fs 或 path)。 每个 target 都包含各种 deployment(部署)/environment(环境)特定的附加项,以满足其需求。具体请参阅 target 可用值在新窗口打开。 #多 target 虽然 webpack 不支持 向 target 属性传入多个字符串,但是可以通过设置两个独立配置,来构建对 library 进行同构: webpack.config.js javascript复制 const path = require('path'); const serverConfig = { target: 'node', output: { path: path.resolve(__dirname, 'dist'), filename: 'lib.node.js' } //… }; const clientConfig = { target: 'web', // <=== 默认为 'web',可省略 output: { path: path.resolve(__dirname, 'dist'), filename: 'lib.js' } //… }; module.exports = [ serverConfig, clientConfig ]; 1234567891011121314151617181920 上述示例中,将会在 dist 文件夹下创建 lib.js 和 lib.node.js 文件。 #资源 从上面选项可以看出,你可以选择部署不同的 target。下面是可以参考的示例和资源: compare-webpack-target-bundles在新窗口打开:测试并查看 webpack target 的绝佳资源。同样包含错误上报。 Boilerplate of Electron-React Application在新窗口打开: 一个关于 electron 主进程和渲染进程构建过程的优秀示例。 #manifest 在使用 webpack 构建的典型应用程序或站点中,有三种主要的代码类型: 你或你的团队编写的源码。 你的源码会依赖的任何第三方的 library 或 "vendor" 代码。 webpack 的 runtime 和 manifest,管理所有模块的交互。 本文将重点介绍这三个部分中的最后部分:runtime 和 manifest,特别是 manifest。 #runtime runtime,以及伴随的 manifest 数据,主要是指:在浏览器运行过程中,webpack 用来连接模块化应用程序所需的所有代码。它包含:在模块交互时,连接模块所需的加载和解析逻辑。包括:已经加载到浏览器中的连接模块逻辑,以及尚未加载模块的延迟加载逻辑。 #manifest 一旦你的应用在浏览器中以 index.html 文件的形式被打开,一些 bundle 和应用需要的各种资源都需要用某种方式被加载与链接起来。在经过打包、压缩、为延迟加载而拆分为细小的 chunk 这些 webpack `优化`在新窗口打开 之后,你精心安排的 /src 目录的文件结构都已经不再存在。所以 webpack 如何管理所有所需模块之间的交互呢?这就是 manifest 数据用途的由来…… 当 compiler 开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "manifest",当完成打包并发送到浏览器时,runtime 会通过 manifest 来解析和加载模块。无论你选择哪种 模块语法在新窗口打开,那些 import 或 require 语句现在都已经转换为 __webpack_require__ 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够检索这些标识符,找出每个标识符背后对应的模块。 #问题 所以,现在你应该对 webpack 在幕后工作有一点了解。“但是,这对我有什么影响呢?”,你可能会问。答案是大多数情况下没有。runtime 做完成这些工作:一旦你的应用程序加载到浏览器中,使用 manifest,然后所有内容将展现出魔幻般运行结果。然而,如果你决定通过使用浏览器缓存来改善项目的性能,理解这一过程将突然变得极为重要。 通过使用内容散列(content hash)作为 bundle 文件的名称,这样在文件内容修改时,会计算出新的 hash,浏览器会使用新的名称加载文件,从而使缓存无效。一旦你开始这样做,你会立即注意到一些有趣的行为。即使某些内容明显没有修改,某些 hash 还是会改变。这是因为,注入的 runtime 和 manifest 在每次构建后都会发生变化。 查看_管理输出_指南的 manifest 部分在新窗口打开,了解如何提取 manifest,并阅读下面的指南,以了解更多长效缓存错综复杂之处。 #模块热替换(hot module replacement) 模块热替换(HMR - hot module replacement)功能会在应用程序运行过程中,替换、添加或删除 模块在新窗口打开,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度: 保留在完全重新加载页面期间丢失的应用程序状态。 只更新变更内容,以节省宝贵的开发时间。 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。 #在应用程序中 通过以下步骤,可以做到在应用程序中置换(swap in and out)模块: 应用程序要求 HMR runtime 检查更新。 HMR runtime 异步地下载更新,然后通知应用程序。 应用程序要求 HMR runtime 应用更新。 HMR runtime 同步地应用更新。 你可以设置 HMR,以使此进程自动触发更新,或者你可以选择要求在用户交互时进行更新。 #在 compiler 中 除了普通资源,compiler 需要发出 "update",将之前的版本更新到新的版本。"update" 由两部分组成: 更新后的 manifest在新窗口打开 (JSON) 一个或多个 updated chunk (JavaScript) manifest 包括新的 compilation hash 和所有的 updated chunk 列表。每个 chunk 都包含着全部更新模块的最新代码(或一个 flag 用于表明此模块需要被移除)。 compiler 会确保在这些构建之间的模块 ID 和 chunk ID 保持一致。通常将这些 ID 存储在内存中(例如,使用 webpack-dev-server在新窗口打开 时),但是也可能会将它们存储在一个 JSON 文件中。 #在模块中 HMR 是可选功能,只会影响包含 HMR 代码的模块。举个例子,通过 `style-loader`在新窗口打开 为 style 追加补丁。为了运行追加补丁,style-loader 实现了 HMR 接口;当它通过 HMR 接收到更新,它会使用新的样式替换旧的样式。 类似的,当在一个模块中实现了 HMR 接口,你可以描述出当模块被更新后发生了什么。然而在多数情况下,不需要在每个模块中强行写入 HMR 代码。如果一个模块没有 HMR 处理函数,更新就会冒泡(bubble up)。这意味着某个单独处理函数能够更新整个模块树。如果在模块树的一个单独模块被更新,那么整组依赖模块都会被重新加载。 有关 module.hot 接口的详细信息,请查看 HMR API 页面在新窗口打开。 #在 runtime 中 这件事情比较有技术性……如果你对其内部不感兴趣,可以随时跳到 HMR API 页面在新窗口打开 或 HMR 指南在新窗口打开。 对于模块系统运行时(module system runtime),会发出额外代码,来跟踪模块 parents 和 children 关系。在管理方面,runtime 支持两个方法 check 和 apply。 check 方法,发送一个 HTTP 请求来更新 manifest。如果请求失败,说明没有可用更新。如果请求成功,会将 updated chunk 列表与当前的 loaded chunk 列表进行比较。每个 loaded chunk 都会下载相应的 updated chunk。当所有更新 chunk 完成下载,runtime 就会切换到 ready 状态。 apply 方法,将所有 updated module 标记为无效。对于每个无效 module,都需要在模块中有一个 update handler,或者在此模块的父级模块中有 update handler。否则,会进行无效标记冒泡,并且父级也会被标记为无效。继续每个冒泡,直到到达应用程序入口起点,或者到达带有 update handler 的 module(以最先到达为准,冒泡停止)。如果它从入口起点开始冒泡,则此过程失败。 之后,所有无效 module 都会被(通过 dispose handler)处理和解除加载。然后更新当前 hash,并且调用所有 accept handler。runtime 切换回 idle 状态,一切照常继续。 #揭示内部原理 打包,是指处理某些文件并将其输出为其他文件的能力。 但是,在输入和输出之间,还包括有 模块在新窗口打开, 入口起点在新窗口打开, chunk, chunk 组和许多其他中间部分。 #主要部分 项目中使用的每个文件都是一个 模块在新窗口打开 ./index.js js复制 import app from './app.js'; 1 ./app.js js复制 export default 'the app'; 1 通过互相引用,这些模块会形成一个图(ModuleGraph)数据结构。 在打包过程中,模块会被合并成 chunk。 chunk 合并成 chunk 组,并形成一个通过模块互相连接的图(ModuleGraph)。 那么如何通过以上来描述一个入口起点:在其内部,会创建一个只有一个 chunk 的 chunk 组。 ./webpack.config.js js复制 module.exports = { entry: './index.js' }; 123 这会创建出一个名为 main 的 chunk 组(main 是入口起点的默认名称)。 此 chunk 组包含 ./index.js 模块。随着 parser 处理 ./index.js 内部的 import 时, 新模块就会被添加到此 chunk 中。 另外的一个示例: ./webpack.config.js js复制 module.exports = { entry: { home: './home.js', about: './about.js' } }; 123456 这会创建出两个名为 home 和 about 的 chunk 组。 每个 chunk 组都有一个包含一个模块的 chunk:./home.js 对应 home,./about.js 对应 about 一个 chunk 组中可能有多个 chunk。例如,SplitChunksPlugin在新窗口打开 会将一个 chunk 拆分为一个或多个 chunk。 #chunk chunk 有两种形式: initial(初始化) 是入口起点的 main chunk。此 chunk 包含为入口起点指定的所有模块及其依赖项。 non-initial 是可以延迟加载的块。可能会出现在使用 动态导入(dynamic imports)在新窗口打开 或者 SplitChunksPlugin在新窗口打开 时。 每个 chunk 都有对应的 asset(资源)。资源,是指输出文件(即打包结果)。 webpack.config.js js复制 module.exports = { entry: './src/index.jsx' }; 123 ./src/index.jsx js复制 import React from 'react'; import ReactDOM from 'react-dom'; import('./app.jsx').then(App => { ReactDOM.render(<App />, root); }); 123456 这会创建出一个名为 main 的 initial chunk。其中包含: ./src/index.jsx react react-dom 以及除 ./app.jsx 外的所有依赖 然后会为 ./app.jsx 创建 non-initial chunk,这是因为 ./app.jsx 是动态导入的。 Output: /dist/main.js - 一个 initial chunk /dist/394.js - non-initial chunk 默认情况下,这些 non-initial chunk 没有名称,因此会使用唯一 ID 来替代名称。 在使用动态导入时,我们可以通过使用 magic comment(魔术注释)在新窗口打开 来显式指定 chunk 名称: js复制 import( /* webpackChunkName: "app" */ './app.jsx' ).then(App => { ReactDOM.render(<App />, root); }); 123456 Output: /dist/main.js - 一个 initial chunk /dist/app.js - non-initial chunk #output(输出) 输出文件的名称会受配置中的两个字段的影响: `output.filename`在新窗口打开 - 用于 initial chunk 文件 `output.chunkFilename`在新窗口打开 - 用于 non-initial chunk 文件 在某些情况下,使用 initial 和 non-initial 的 chunk 时,可以使用 output.filename。 这些字段中会有一些 占位符在新窗口打开。常用的占位符如下: [id] - chunk id(例如 [id].js -> 485.js) [name] - chunk name(例如 [name].js -> app.js)。如果 chunk 没有名称,则会使用其 id 作为名称 [contenthash] - 输出文件内容的 md4-hash(例如 [contenthash].js -> 4ea6ff1de66c537eb9b2.js)
2021年01月13日
129 阅读
0 评论
0 点赞
1
2
...
6