title: "Notes on work projects (in Chinese)" description: "" added: "Oct 19 2021" tags: [web]
pages/*
),借助 webpack 多⼊⼝配置,打包成多个不同的子项目产出,总体结构来自于一个比较老的模板 https://github.com/vuejs-templates/webpacksrc/pages
得到所有入口。HtmlWebpackPlugin
指定它的模板,并注入它需要的 chunks (对应每一个 entry 打包出的 js),本地直接通过 localhost/xx.html
访问,线上通过配置 nginx 路由映射访问 try_files $uri /static/xx.html
chunks
是因为项目是多 entry 会生成多个编译后的 js 文件,chunks 决定使用哪些 js 文件,如果没有指定默认会全部引用。inject
值为 true,表明 chunks js 会被注入到 html 文件的 head 中,以 script defer 标签的形式引入。对于 css, 使用 mini-css-extract-plugin
从 bundle 中分离出单独的 css 文件并在 head 中以 link 标签引入。(extract-text-webpack-plugin 是老版本 webpack 用来提取 css 文件的插件,从 webpack v4 被 mini-css-extract-plugin 替代)request
, API
, i18n
这些对象挂载在 window 对象上,子组件中不需要单独引用。router
文件,这是子项目的路由,而且每个路由加载的 component 都是异步获取,在访问该路由时按需加载。dist/
)会 emit 出所有 HtmlWebpackPlugin
生成的 html 文件(这也是浏览器访问的入口),相对每个 entry 打包出的 js 文件 js/[name].[chunkhash].js
(对应 output.filename),所有异步加载的组件 js js/[id].[chunkhash].js
(对应 output.chunkFilename)。这些 chunk 基本来自 vue-router 配置的路由 component: resolve => require(['@/components/foo'], resolve)
,这样懒加载的组件会生成一个 js 文件。copy-webpack-plugin
用来把那些已经在项目目录中的文件(比如 public/
或 static/
)拷贝到打包后的产出中,这些文件不需要 build,不需要 webpack 的处理。另外可以使用 ignore: ["**/file.*", "**/ignored-directory/**"]
这样的语法忽略一些文件不进行拷贝。url-loader
结合 limit
的设置,如果资源比较大会默认使用 file-loader
生成 img/[name].[hash:7].[ext]
这样的文件;如果资源小,会自动转成 base64。(DEPREACTED for v5: please consider migrating to asset modules)performance
属性用来设置当打包资源和入口文件超过一定的大小给出警告或报错,可以分别设置它们的上限和哪些文件被检查。具体多大的文件算“过大”,则需要用到 maxEntrypointSize
和 maxAssetSize
两个参数,单位是 byte。terser-webpack-plugin
来压缩 JS,webpack 5 自带,但如果需要自定义配置,那么仍需要安装该插件,在 webpack 配置文件里设置 optimization
来引用这个插件。HtmlWebpackPlugin
里设置 minify
可以压缩 HTML,production 模式下是默认是 true(会使用 html-minifier-terser
插件去掉空格、注释等),自己传入一个 minify 对象,可以定制化压缩设置。uglifyjs-webpack-plugin
,里面传入 compress
定制化压缩设置。比如有的项目没有 console 输出,可能就是因为这里设置了 drop_console
。friendly-errors-webpack-plugin
简化命令行的输出,可以只显示构建成功、警告、错误的提示,从而优化命令⾏的构建日志。localhost:3000
,后端是 localhost:8082
,那么后端通过 request.getHeader("Host")
获取的依旧是 localhost:3000
。如果设置了 changeOrigin: true
,那么后端才会看到的是 localhost:8082
, 代理服务器会根据请求的 target 地址修改 Host(这个在浏览器里看请求头是看不到改变的)。如果某个接口 404,一般就是这个路径没有配置代理。component: () => import('@/views/About.vue')
可以做到 code-splitting,这样会单独产出一个文件名为 About.[hash].js
的 chunk 文件,路由被访问时才会被加载。Vue npm 包有不同的 Vue.js 构建版本,可以在 node_modules/vue/dist
中看到它们,大致包括完整版、编译器(编译template)、运行时版本、UMD 版本(通过 <script>
标签直接用在浏览器中)、CommonJS 版本(用于很老的打包工具)、ES Module 版本。总的来说,Runtime + Compiler 版本是包含编译代码的,可以把编译过程放在运行时做,如果需要在客户端编译模板 (比如传入一个字符串给 template 选项),就需要加上编译器的完整版。Runtime 版本不包含编译代码,需要借助 webpack 的 vue-loader
事先把 *.vue
文件内的模板编译成 render
函数,在最终打好的包里实际上是不需要编译器的,只用运行时版本即可。
// Using Runtime + Compiler
new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
})
// build/webpack.base.conf.js
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js',
}
}
// Using Runtime-only
new Vue({
el: '#app',
router,
render: h => h(App)
})
Vue 是如何被编译的详细介绍:https://vue-compiler.iamouyang.cn/template/baseCompile.html
Vue 3 在 2022 年 2 月代替 Vue 2 成为 Vue 的默认版本。
npm create vite
可创建一个基于 Vite 的基础空项目。npm init vue@3
然后根据命令行的提示操作。vue create hello-vue3
根据提示创建项目。(Vue CLI is in Maintenance Mode. For new projects, it is now recommended to use create-vue to scaffold Vite-based projects.)The Vue Language Tools are essential for providing language features such as autocompletion, type checking, and diagnostics when working with Vue’s SFCs. While Volar
powers the language tools, the official extension for Vue is titled Vue - Official
now on the VSCode marketplace.
Vue DevTools is designed to enhance the Vue developer experience. There are multiple options to add this tool to your projects by Vite plugin, Standalone App, or Chrome Extension. Note that The v7 version of devtools only supports Vue3. If your application is still using Vue2, please install the v6 version.
filename
是对应于 entry 里面的输入文件,经过打包后输出文件的名称。chunkFilename
指未被列在 entry 中,却又需要被打包出来的 chunk 文件的名称(non-initial chunk files),一般是要懒加载的代码。output.filename
的输出文件名是 js/[name].[chunkhash].js
,[name]
根据 entry 的配置推断为 index,所以输出为 index.[chunkhash].js
。output.chunkFilename
默认使用 [id].js
, 会把 [name]
替换为 chunk 文件的 id 号。js/
to the filename in output.filename
, webpack will write bundled files to a js sub-directory in the output.path
. This allows you to organize files of a particular type in appropriately named sub-directories.chunkFileName
不能灵活自定义,但可以通过 /* webpackChunkName: "foo" */
这样的 Magic Comments,给 import 语句添加注释来命名 chunk。chunkhash
根据不同的入口文件构建对应的 chunk,生成对应的哈希值,来源于同一个 chunk,则 hash 值就一样。output.path
represents the absolute path for webpack file output in the file system. In other words, path
is the physical location on disk where webpack will write the bundled files.output.publicPath
represents the path from which bundled files should be accessed by the browser. You can load assets from a custom directory (/assets/
) or a CDN (https://cdn.example.com/assets/
). The value of the option is prefixed to every URL created by the runtime or loaders.In a typical application built with webpack, there are three main types of code:
It is necessary to differentiate between Code Splitting and splitChunks. Code splitting is a feature native to Webpack, which uses the import('package')
statement to move certain modules to a new Chunk. SplitChunks is essentially a further splitting of the Chunks produced by code splitting.
Code splitting also has some drawbacks. There’s a delay between loading the entry point chunk (e.g., the top-level app with the client-side router) and loading the initial page (e.g., home). The way to improve this is by injecting a small script in the head of the HTML, when executed, it preloads the necessary files for the current path by manually adding them to the HTML page as link
rel="preload"
.
After code splitting, many Chunks will be created, and each Chunk will correspond to one ChunkGroup. SplitChunks is essentially splitting Chunk into more Chunks to form a group and to load groups together, for example, under HTTP/2, a Chunk could be split into a group of 20 Chunks for simultaneous loading.
chunks: 'all' | 'initial' | 'async':
all
means both dynamically imported modules and statically imported modules will be selected for optimization.initial
means only statically imported modules; async
means only dynamically imported modules.['.js', '.json', '.wasm']
.['index']
."@": path.join(__dirname, 'src')
.['node_modules']
,即从 node_modules 目录下寻找。css-loader
takes a CSS file and returns the CSS with @import
and url(...)
resolved. It doesn't actually do anything with the returned CSS and is not responsible for how CSS is ultimately displayed on the page.style-loader
takes those styles and creates a <style>
tag in the page's <head>
element containing those styles. The order of CSS insertion is completely consistent with the import order.sass-loader
with the css-loader
and the style-loader
to immediately apply all styles to the DOM or the mini-css-extract-plugin
to extract it into a separate file.Webpack goes through all the import
and require
files in your project, and for all those files which have a .png|.jpg|.gif
extension, it uses as an input to the webpack file-loader
. For each of these files, the file loader emits the file in the output directory and resolves the correct URL to be referenced. Note that this config only works for webpack 4, and Webpack 5 has deprecated the file-loader
. If you are using webpack 5 you should change file-loader
to asset/resource
.
Webpack 4 also has the concept url-loader
. It first base64 encodes the file and then inlines it. It will become part of the bundle. That means it will not output a separate file like file-loader
does. If you are using webpack 5, then url-loader
is deprecated and instead, you should use asset/inline
.
Loaders are transformations that are applied to the source code of a module. When you provide a list of loaders, they are applied from right to left, like
use: ['third-loader', 'second-loader', 'first-loader']
. This makes more sense once you look at a loader as a function that passes its result to the next loader in the chainthird(second(first(source)))
.
webpack-dev-server
doesn't write any output files after compiling. Instead, it keeps bundle files in memory and serves them as if they were real files mounted at the server's root path.webpack-dev-middleware
is an express-style development middleware that will emit files processed by webpack to a server. This is used in webpack-dev-server
internally.webpack-dev-server
from the mobile in local network: run webpack-dev-server
with --host 0.0.0.0
, which lets the server listen for requests from the network (all IP addresses on the local machine), not just localhost. But Chrome won't access http://0.0.0.0:8089
(Safari can open). It's not the IP, it just means it is listening on all the network interfaces, so you can use any IP the host has.--watch
and --hot
webpack --watch
: watch for the file changes and compile again when the source files changes. webpack-dev-server
uses webpack's watch mode by default.webpack-dev-server --hot
: add the HotModuleReplacementPlugin to the webpack configuration, which will allow you to only reload the component that is changed instead of doing a full page refresh.
watchOptions: {
ignored: /node_modules/,
// 监听到变化发生后会等 300ms 再去执行,默认300ms
aggregateTimeout: 300,
// 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒 1000 次
poll: 1000,
}
terser-webpack-plugin
out of the box. optimization.minimize
is set to true
by default, telling webpack to minimize the bundle using the TerserPlugin
.Tree shaking means that unused modules will not be included in the bundle (The term was popularized by Rollup). In order to take advantage of tree shaking, you must use ES2015 module syntax. Ensure no compilers transform your ES2015 module syntax into CommonJS modules (this is the default behavior of the popular Babel preset @babel/preset-env
).
Webpack do tree-shake only happens when you're using a esmodule, while lodash is not. Alternatively, you can try to use lodash-es written in ES6.
- import cloneDeep from "lodash/cloneDeep"
- import { camelCase } from "lodash-es"
- import * as _ from "lodash-es"
// babel.config.js
// keep Babel from transpiling ES6 modules to CommonJS modules
export default {
presets: [
[
"@babel/preset-env", {
modules: false
}
]
]
}
// if you're using Webpack and `babel-loader`
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
type: 'module'
}
}
}
The sideEffects
property of package.json
declares whether a module has side effects on import. When side effects are present, unused modules and unused exports may not be tree shaken due to the limitations of static analysis.
It will create an interactive treemap visualization of the contents of all your bundles when you build the application. There are two ways to configure webpack bundle analyzer in a webpack project. Either as a plugin or using the command-line interface.
// Configure the webpack bundle analyzer plugin
// npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
See how fast (or not) your plugins and loaders are, so you can optimise your builds. This plugin measures your webpack build speed, giving an output in the terminal.
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
plugins: [new MyPlugin(), new MyOtherPlugin()],
});
Webpack 5 fails if using smp.wrap()
the config, with the error: "You forgot to add mini-css-extract-plugin
plugin". As a hacky workaround, you can append MiniCssExtractPlugin
after wrapping with speed-measure-webpack-plugin
.
Webpack is extensible with "loaders" that can be added to handle particular file formats.
typescript
and ts-loader as devDependencies.ts-loader
is to act as a drop-in replacement for the tsc
command, so it respects the options in tsconfig.json
.babel-loader
with ts-loader
. We need to compile a .ts
file using ts-loader
first and then using babel-loader
.ts-loader
does not write any file to disk. It compiles TypeScript files and passes the resulting JavaScript to webpack, which happens in memory.TypeScript doesn't understand .vue
files - they aren't actually Typescript modules. So it will throw an error when you try to import Foo.vue
. The solution is shims-vue.d.ts
in src
directory. The filename does not seem to be important, as long as it ends with .d.ts
. TypeScript looks for .d.ts
files in the same places it looks for your regular .ts
files. It basically means, "every time you import a module with the name *.vue
, then treat it as if it had these contents, and the type of Foo
will be Vue."
// shims-vue.d.ts
declare module "*.vue" {
import Vue from 'vue';
export default Vue;
}
If that doesn't help, make sure the module you are trying to import is tracked by TypeScript. It should be covered in your include
array setting and not be present in the exclude
array in your tsconfig.json
file.
{
"compilerOptions": {
// ...
},
"include": ["src/**/*"],
"exclude": ["node_modules", "src/**/*.spec.ts"]
}
Above was created in the days before Vue shipped with TypeScript out of the box. Now the best path to get started is through the official CLI.
Transpiling is an expensive process and many projects have thousands of lines of code imported in that babel would need to run over. Your node_modules
should already be runnable without transpiling and there are simple ways to exclude your node_modules
but transpile any code that needs it.
{
test: /\.js$/,
exclude: /node_modules\/(?!(my_main_package\/what_i_need_to_include)\/).*/,
use: {
loader: 'babel-loader',
options: ...
}
}
git-revision-webpack-plugin generates VERSION and COMMITHASH files during build.
const GitRevisionPlugin = require('git-revision-webpack-plugin');
const gitRevisionPlugin = new GitRevisionPlugin();
plugins: [
new DefinePlugin({
'VERSION': JSON.stringify(gitRevisionPlugin.version()),
'COMMITHASH': JSON.stringify(gitRevisionPlugin.commithash()),
'BRANCH': JSON.stringify(gitRevisionPlugin.branch()),
}),
]
The
DefinePlugin
allows you to create global constants that are replaced at compile time, commonly used to specify environment variables or configuration values that should be available throughout your application during the build process. For example, you might use it to defineprocess.env.NODE_ENV
as 'production' or 'development' which webpack will literally replace in your code during bundling.
npm link
映射到全局的 node_modules,然后被其他项目 npm link C
引用。(关于 npm link
的使用场景可以看看 https://github.com/atian25/blog/issues/17)npm build
和 npm link
,之后再进入项目 A 本身,执行 npm link C
,npm build
等项目本身的构建。server-static
存放 build 后的静态文件,它的上线脚本里并不含构建过程,只是在拷贝仓库中的 server-static
目录。因为源文件中会有对组件库的引用 import foo from 'C/dist/foo.js
,本地 build 时组件库已经被打包进去。The build job uses Kaniko (a tool for building Docker images in Kubernetes). Its main task is to build a Docker image.
prefixOss=`echo ${CI_COMMIT_REF_NAME} | sed -e "s/\_/-/g" -e "s/\//-/g"`
[ -z ${CI_COMMIT_TAG} ] && sed -i -E "s/^ \"version\": \"[0-9\.]+\"/ \"version\": \"0.0.0-${prefixOss}-${CI_COMMIT_SHORT_SHA}\"/g" package.json
The above checks if the environment variable CI_COMMIT_TAG
is empty (meaning it's not a tag build). If that's the case, it uses sed to perform an in-place replacement in the package.json
file. Specifically, it looks for lines that start with "version": "[0-9.]+" and replaces them with a new version format 0.0.0-${prefixOss}-${CI_COMMIT_SHORT_SHA}
. This script appears to be adjusting versioning and paths based on the branch or tag being built in a CI/CD pipeline.
The
s
command is for substitute, to replace text -- the format iss/[text to select]/[text to replace]/
. For example,sed 's/target/replacement/g' file.txt
will globally substitute the wordtarget
withreplacement
.
rimraf
command is an alternative to the Linux command rm -rf
)webpack()
传入配置 webpack.prod.conf
和一个回调函数,webpack stats 对象 作为回调函数的参数,可以通过它获取到 webpack 打包过程中的信息,使用 process.stdout.write(stats.toString(...))
输出到命令行中 (console.log
in Node is just process.stdout.write
with formatted output)有些 url 请求是后端直出页面返回 html,通过类似 render_to_response(template, data)
的方法,将数据打到模板中,模板里会引用 xx/static/js
路径下的 js 文件,这些 js 使用 require 框架,导入需要的其他 js 文件或 tpl 模板,再结合业务逻辑使用 underscore 的 template 方法(_.template(xx)
)可以将 tpl 渲染为 html,然后被 jquery .html()
方法插入到 DOM 中。
/web?old=1
后端会返回 html 扫码登录页面,这里面有一个 /static/vue/login.js?_dt=xxxxx
,里面有登录和加载网页版首页的逻辑,这样就会展示出 h5 中的页面,其中的 iframe 可以嵌套任意 pc 或 h5 中的页面(只要有路由支持),这个 iframe 的链接自然也可以被单独访问。router.start(App, 'app')
创建 vue 实例并进行挂载 (https://github.com/vuejs/vue-router/blob/1.0/docs/en/api/start.md),这之后才会被前端路由接管。而且这个 html 只能在手机端访问(根据 ua),否则会跳到 web 端的逻辑。# urls.py
urlpatterns = [
url(r'^v/index', foo.index),
url(r'^web', foo.web),
]
# views.py
# def index(request):
return render_to_response('foo/vue_index.html', context)
# def web(request):
return render_to_response('foo/login.html', context)
# import scripts in above template html
<script>
var isInIframe = window.frames.length !== parent.frames.length;
var ua = window.navigator.userAgent;
if (!isInIframe && !ua.toLowerCase().match(/micromessenger|android|iphone/i)) {
window.location.href = '/web/?next=' + window.location.pathname;
}
</script>
<script src="https://cdn.example.com/assets/login.js"></script>
关于 iframe 的配置:https://iframegenerator.top
op
代表不同的操作,比如 requestlogin 会返回微信生成的二维码(包括 qrcode, ticket, expire_seconds 等), 扫码成功返回类型是 loginsuccess,并附带 OpenID, UnionID, Name, UserID, Auth 等信息,前端拿到这些信息后可以请求后端登录的 http 接口,拿到 sessionid,并被种在 cookie 里。常规的扫码登录原理(涉及 PC 端、手机端、服务端):
- PC 端携带设备信息向服务端发起生成二维码的请求,生成的二维码中封装了 uuid 信息,并且跟 PC 设备信息关联起来,二维码有失效时间。PC 端轮询检查是否已经扫码登录。
- 手机(已经登录过)进行扫码,将手机端登录的信息凭证(token)和二维码 uuid 发送给服务端,此时的手机一定是登录的,不存在没登录的情况。服务端生成一个一次性 token 返回给移动端,用作确认时候的凭证。
- 移动端携带上一步的临时 token 确认登录,服务端校对完成后,会更新二维码状态,并且给 PC 端一个正式的 token,后续 PC 端就是持有这个 token 访问服务端。
- 流程参考 https://github.com/ahu/scan_qrcode_login/blob/master/qr.js
常规的密码存储:
A Rainbow Table is a precomputed table of hashes and their inputs. This allows an attacker to simply look up the hash in the table to find the input. This means that if an attacker gets access to your database, they can simply look up the hashes to find the passwords.
To protect against this, password hashing algorithms use a salt. A salt is a random string that is added to the password before hashing. This means that even if two users have the same password, their hashes will be different. This makes it impossible for an attacker to use a rainbow table to find the passwords. In fact a common practice is to simply append the salt to the hash. This will make it so that the salt is always available when you need to verify the password.
A great library for generating bcrypt hashes is bcryptjs which will generate a random salt for you. This means that you don't need to worry about generating a salt and you can simply store the whole thing as is. Then when the user logs in, you provide the stored hash and the password they provide to bcryptjs's
compare
function will verify the password is correct.
import { compare, hash } from 'bcryptjs';
import { SignJWT } from 'jose';
// Takes a string as input, and returns a Uint8Array containing UTF-8 encoded text
const key = new TextEncoder().encode(process.env.AUTH_SECRET);
const SALT_ROUNDS = 10; // Salt length to generate
export async function hashPassword(password) {
return hash(password, SALT_ROUNDS);
}
export async function comparePasswords(plainTextPassword, hashedPassword) {
return compare(plainTextPassword, hashedPassword);
}
export async function signToken(payload: SessionData) {
// https://github.com/panva/jose/blob/main/docs/classes/jwt_sign.SignJWT.md
return await new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('1 day from now')
.sign(key);
}
export async function setSession(user: NewUser) {
const expiresInOneDay = new Date(Date.now() + 24 * 60 * 60 * 1000);
const session: SessionData = {
user: { id: user.id! },
expires: expiresInOneDay.toISOString(),
};
const encryptedSession = await signToken(session);
cookies().set('session', encryptedSession, {
expires: expiresInOneDay,
httpOnly: true,
secure: true, // Only over HTTPS
sameSite: 'strict', // Prevent CSRF
});
}
申请公众号/小程序的时候,都有一个 APPID 作为当前账号的标识,OpenID 就是用户在某一公众平台下的标识(用户微信号和公众平台的 APPID 两个数据加密得到的字符串)。如果开发者拥有多个应用,可以通过获取用户基本信息中的 UnionID 来区分用户的唯一性,因为同一用户,在同一微信开放平台下的不同应用,UnionID 应是相同的,代表同一个人,当然前提是各个公众平台需要先绑定到同一个开放平台。OpenID 同一用户同一应用唯一,UnionID 同一用户不同应用唯一,获取用户的 OpenID 是无需用户同意的,获取用户的基本信息则需要用户同意。
向用户发起授权申请,即打开如下页面: https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
appid
是公众号的唯一标识。redirect_uri
替换为回调页面地址,用户授权完成后,微信会帮你重定向到该地址,并携带相应的参数如 code
,回调页面所在域名必须与后台配置一致。在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中配置授权回调域名。scope
根据业务需要选择 snsapi_base
或 snsapi_userinfo
。其中 snsapi_base
为静默授权,不弹出授权页面,直接跳转,只能获取用户的 openid
,而 snsapi_userinfo
会弹出授权页面,需要用户同意,但无需关注公众号,可在授权后获取用户的基本信息。(对于已关注公众号的用户,如果用户从公众号的会话或者自定义菜单进入本公众号的网页授权页,即使是 scope
为 snsapi_userinfo
,也是静默授权,用户无感知。)state
不是必须的,重定向后会带上 state
参数,开发者可以填写 a-zA-Z0-9 的参数值,最多 128 字节。redirect_uri/?code=CODE&state=STATE
,code
作为换取 access_token
的票据,每次用户授权带上的 code
不一样,code
只能使用一次,5分钟未被使用自动过期。code
后,请求 https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code 获取 access_token
和 openid
(未关注公众号时,用户访问公众号的网页,也会产生一个唯一的 openid)。如果 scope
为 snsapi_userinfo
还会同时获得到 unionid
。snsapi_userinfo
,则此时可以请求 https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN 拉取用户信息,比如用户昵称、头像、unionid
等,不再返回用户性别及地区信息。secret
和获取到的 access_token
安全级别都非常高,必须只保存在服务器,不允许传给客户端。后续刷新 access_token
以及通过 access_token
获取用户信息等步骤,也必须从服务器发起。
- 微信公众平台接口测试帐号申请: https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
- 某个公众号的关注页面地址为 https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MzI0NDA2OTc2Nw==#wechat_redirect 其中 biz 字符串是微信公众号标识,在浏览器打开该公众号下的任意一篇文章,查看网页源代码,搜索
var biz
这样的关键字即可得到。
微信授权也符合通常的 OAuth 流程:
You first need to register your app with your provider to get the required credentials. You’ll be asked to define a callback URL or a redirect URI.
微信外网页通过小程序链接 URL Scheme,微信内通过微信开放标签,且微信内不会直接拉起小程序,需要手动点击按钮跳转。这是官方提供的一个例子 https://postpay-2g5hm2oxbbb721a4-1258211818.tcloudbaseapp.com/jump-mp.html 可以用手机浏览器查看效果,直接跳转小程序。
<wx-open-launch-weapp>
,提供要跳转小程序的原始 ID 和路径,标签内插入自定义的 html 元素。开放标签会被渲染成一个 iframe,所以外部的样式是不会生效的。另外在开放标签上模拟 click 事件也不生效,即不可以在微信内不通过点击直接跳转小程序。可以监听 <wx-open-launch-weapp>
元素的 launch
事件,用户点击跳转按钮并对确认弹窗进行操作后触发。微信小程序相关的仓库,比如 WeUI 组件库、微信小程序示例、computed / watch 扩展等: https://github.com/wechat-miniprogram
国产 APP 各自套壳 Chromium 内核版本,最大的问题就是更新不及时,而且大多被改造过。
Web Inspector
.Show Develop menu in menu bar
.Develop menu
. This will open a Web Inspector window on your Mac.Vue.use
使用自定义的插件。Vue.http.get()
,在一个组件内使用 this.$http.get()
可以定义 inteceptor 在请求发送前和接收响应前做一些处理,比如设置业务相关的请求头、添加 CSRF token、请求加 loading 状态、query 参数加时间戳等。
Vue.http.interceptors.push((request, next) => {
// 请求发送前的处理逻辑(比如判断传入的 request.no_loading 是否显示 loading)
// if (request.method === 'GET') {...}
// if (request.method === 'POST') {...}
next((response) => {
// 请求结果返回给 successCallback 或 errorCallback 之前,根据 `response.ok` 或 `response.status` 加一些处理逻辑
// ...
return response
})
});
axios.defaults.headers['xyz'] = 'abc'
这样的方式添加需要的请求头注意 Axios 遇到 302 的返回:重定向直接被浏览器拦截处理,浏览器 redirect 后,被视为 Axios 发起了跨域请求,所以抛异常。Axios 捕获异常,进入 catch 逻辑。
const handleResponse = (res) => {
if(res.headers && res.headers['set-auth']) {
window.Authorization = res.headers['set-auth'];
}
// 之后根据状态码做不同处理...
}
export default {
get(url, params) {
// 统一加请求头
axios.defaults.headers['X-Client'] = 'web';
if (window.Authorization) {
axios.defaults.headers['Authorization'] = 'Bearer ' + window.Authorization;
}
return axios
.get(url)
.then(function(response) {
return response
})
.then(handleResponse) // 统一处理 redirect, 赋值 location.href
.catch(errorResponseGet) // 统一处理错误码 4xx, 5xx
},
post(url, params) {
// ...
}
}
// Show progress of Axios during request
await axios.get('https://fetch-progress.anthum.com/30kbps/images/sunrise-baseline.jpg', {
onDownloadProgress: progressEvent => {
const percentCompleted = Math.floor(progressEvent.loaded / progressEvent.total * 100)
setProgress(percentCompleted)
}
})
.then(res => {
console.log("All DONE")
return res.data
})
useRequest
接收一个 service 函数,service 是一个异步的请求函数,换句话说,还可以使用 axios 来获取数据,然后返回一个 Promise。useRequest
会返回 data、loading、error 等,它们的值会根据请求状态和结果进行修改。返回的 run 方法,可以手动触发 service 请求。API 版本可以放在两个地方: 在 url 中指定 API 的版本,例如 example.com/api/v1
,这样不同版本的协议解析可以放在不同的服务器上,不用考虑协议兼容性,开发方便,升级也不受影响。另一种是放在 HTTP header 中,url 显得干净,符合 RESTful 惯例,毕竟版本号不属于资源的属性。缺点是需要解析头部,判断返回。
URI 中尽量使用连字符 -
代替下划线 _
的使用,连字符用来分割 URI 中出现的单词,提高 URI 的可读性。下划线会和链接的样式冲突重叠。URI 是对大小写敏感的,为了避免歧义,我们尽量用小写字符。但主机名(Host)和协议名(Scheme)对大小写是不敏感的。
阿里云 CDN 对于文件是否支持缓存是以 X-Cache
头部来确定,缓存时间是以 X-Swift-CacheTime
头部来确认。
Age
表示该文件在 CDN 节点上缓存的时间,单位为秒。只有文件存在于节点上 Age 字段才会出现,当文件被刷新后或者文件被清除的首次访问,在此前文件并未缓存,无 Age 头部字段。当 Age 为 0 时,表示节点已有文件的缓存,但由于缓存已过期,本次无法直接使用该缓存,需回源校验。X-Swift-SaveTime
该文件是在什么时间缓存到 CDN 节点上的。(GMT时间,Greenwich Mean Time Zone)X-Swift-CacheTime
该文件可以在 CDN 节点上缓存多久,是指文件在 CDN 节点缓存的总时间。通过 X-Swift-CacheTime – Age
计算还有多久需要回源刷新。阿里云 CDN 在全球拥有 3200+ 节点。中国内地拥有 2300+ 节点,覆盖 31 个省级区域。
- CDN 节点是指与最终接入用户之间具有较少中间环节的网络节点,对最终接入用户有相对于源站而言更好的响应能力和连接速度。当节点没有缓存用户请求的内容时,节点会返回源站获取资源数据并返回给用户。阿里云 CDN 的源站可以是对象存储OSS、函数计算、自有源站(IP、源站域名)。
- 默认情况下将使用 OSS 的 Bucket 地址作为 HOST 地址(如
***.oss-cn-hangzhou.aliyuncs.com
)。如果源站 OSS Bucket 绑定了自定义域名(如origin.developer.aliyundoc.com
),则需要配置回源 HOST 为自定义域名。- 加速域名即网站域名、是终端用户实际访问的域名。CNAME 域名是 CDN 生成的,当您在阿里云 CDN 控制台添加加速域名后,系统会为加速域名分配一个
*.*kunlun*.com
形式的 CNAME 域名。- 添加加速域名后,需要在 DNS 解析服务商处,添加一条 CNAME 记录,将加速域名的 DNS 解析记录指向 CNAME 域名,记录生效后该域名所有的请求都将转向 CDN 节点,达到加速效果。CNAME 域名将会解析到具体哪个节点 IP 地址,将由 CDN 的调度系统综合多个条件来决定。
The input
event is fired every time the value of the element changes. This is unlike the change
event, which only fires when the value is committed, such as by pressing the enter key or selecting a value from a list of options. Note that onChange
in React behaves like the browser input
event. (in React it is idiomatic to use onChange
instead of onInput
)
The order in which the events are fired: mousedown
--> mouseup
--> click
. When you add a blur
event, it is actually fired before the mouseup
event and after the mousedown
event of the button. Refer to https://codepen.io/mudassir0909/full/qBjvzL
Reading content with textContent
is much faster than innerText
(innerText
had the overhead of checking to see if the element was visible or not yet). The insertAdjacentHTML
method is much faster than innerHTML
because it doesn’t have to destroy the DOM first before inserting.
HTML files input change event doesn't fire upon selecting the same file. You can put this.value = null
at the end of the onchange
event, which will reset the input's value and trigger the onchange
event again.
If we are appending each list item to the DOM as we create it, this is inefficient because the DOM is updated each time we append a new list item. Instead, we can create a document fragment using document.createDocumentFragment()
and append all of the list items to the fragment. Then, we can append the fragment to the DOM. This way, the DOM is only updated once.
Vue parent component will wait for its children to mount before it mounts its own template to the DOM. The order should be: parent created -> child created -> child mounted -> parent mouted.
Sometimes I need to detect whether a click happens inside or outside of a particular element.
window.addEventListener('mousedown', e => {
// Get the element that was clicked
const clickedEl = e.target;
// `el` is the element you're detecting clicks outside of
// https://developer.mozilla.org/en-US/docs/Web/API/Node/contains
if (el.contains(clickedEl)) {
// Clicked inside of `el`
} else {
// Clicked outside of `el`
}
});
Change the style of :before
pseudo-elements using JS. (It's not possible to directly access pseudo-elements with JS as they're not part of the DOM.)
let style = document.querySelector('.foo').style;
style.setProperty('--background', 'red');
.foo::before {
background: var(--background);
content: '';
display: block;
width: 200px;
height: 200px;
}
The default behavior of scrollIntoView()
is that the top of the element will be aligned to the top of the visible area of the scrollable ancestor. If it shifts the complete page, you could either call it with the parameter false
to indicate that it should aligned to the bottom of the ancestor or just use scrollTop
instead of scrollIntoView()
.
let target = document.getElementById("target");
target.parentNode.scrollTop = target.offsetTop;
// can also add the css `scroll-behavior: smooth;`
If several listeners are attached to the same element for the same event type, they are called in the order in which they were added. If stopImmediatePropagation()
is invoked during one such call, no remaining listeners will be called.
To detect if a user has their keyboard's caps lock turn on, we'll employ KeyboardEvent's getModifierState
method (which returns the current state of the specified modifier key, true
if the modifier is active):
document.querySelector('input[type=password]').addEventListener('keyup', function (keyboardEvent) {
const capsLockOn = keyboardEvent.getModifierState('CapsLock');
if (capsLockOn) {
// Warn the user that their caps lock is on
}
});
npmmirror 已内置支持类似 unpkg cdn 解析能力,可以简单理解为访问 unpkg 地址时,在回源服务里面根据 URL 参数,去 npm registry 下载对应的 npm 包,解压后响应对应的文件内容。即只需要遵循约定的 URL 进行访问,即可在页面中加载任意 npm 包里面的文件内容。
# 获取目录信息 /${pkg}/${versionOrTag}/files?meta
https://registry.npmmirror.com/antd/5.5.2/files?meta
# 获取文件内容 /${pkg}/${versionOrTag}/files/${path}
https://registry.npmmirror.com/antd/5.5.0/files/lib/index.js
# 获取入口文件内容 /${pkg}/${versionOrTag}/files
https://registry.npmmirror.com/antd/latest/files
播放器与字幕的跨域问题:由于加了字幕,但字幕地址是跨域的,所以播放器标签上必须加 crossorigin="anonymous"
也就是改变了原来请求视频的方式(no-cors 是 HTML 元素发起请求的默认状态;现在会创建一个状态为 anonymous 的 cors 请求,不发 cookie),此时服务端必须响应 Access-Control-Allow-Origin
才可以。『播放器不设置跨域 只给字幕配 cors 响应头』这个方案是不行的,因为必须要先发一个 cors 请求才可以,服务端配置的响应头才有用处。
crossorigin="anonymous"
to the video tag to allow load VTT files from different domains.<script type="module"
>) require the use of the CORS protocol for cross-origin fetching.防止重复提交:前端防抖,按钮点击后立即禁用。后端接口幂等性设计。
iframe 技术方案(浏览器原生的隔离方案)
Electron是一个集成项目,允许开发者使用前端技术开发桌面端应用。其中 Chromium 基础能力可以让应用渲染 HTML 页面,执行页面的 JS 脚本,让应用可以在 Cookie 或 LocalStorage 中存取数据。Electron 还继承了 Chromium 的多进程架构,分一个主进程和多个渲染进程,主进程进行核心的调度启动,不同的 GUI 窗口独立渲染,做到进程间的隔离,进程与进程之间实现了 IPC 通信。Node.js 基础能力可以让开发者读写本地磁盘的文件,通过 socket 访问网络,创建和控制子进程等。Electron 内置模块可以支持创建操作系统的托盘图标,访问操作系统的剪切板,获取屏幕信息,发送系统通知,收集崩溃报告等。
Node.js(内置 libuv)有自己的消息循环,要想把这个消息循环和应用程序的消息循环合并到一起并不容易。Electron 的做法是创建了一个单独的线程并使用系统调用来轮询 libuv 的 fd (文件描述符),以获得 libuv 的消息,再把消息交给 GUI 主线程,由主线程的消息循环处理 libuv 的消息。
greeting()
方法,根据终端窗口的宽度 process.stdout.columns
显示不同样式的问候语。Promise.all()
同时启动主进程和渲染进程的构建,两者分别有自己的 webpack 配置文件 webpack.main.config
和 webpack.renderer.config
cross-env BUILD_ENV=abc
设置一些环境变量。创建一个 WebpackDevServer,传入 webpack 配置,设置代理,监听某一端口,其实这就是启动一个本地服务,使用浏览器也可以访问构建后的页面,这里只是用 electron 的壳子把它加载进来。对于主进程,也使用了 webpack,设置入口文件用来打包产出。logStats()
函数接收进程名 (Main or Renderer) 和具体输出的内容。main.js
且渲染进程本地服务可以访问,这个时候启动 electron,即通常项目的 npm 脚本会执行 electron .
,这里是通过 Node API,使用 child_process.spawn()
的方式启动 electron 并传入需要的参数,然后对 electron 进程的 stdout 和 stderr 监听,打印对应的日志。Electron doesn't have a built-in way to persist user preferences and other data. electron-store handles that for you, so you can focus on building your app. The data is saved in a JSON file in app.getPath('userData')
.
appData
, which by default points to ~/Library/Application Support
on macOS.userData
(storing your app's configuration files), which by default is the appData directory appended with your app's name.Advantages over localStorage
:
localStorage
only works in the browser process.localStorage
is not very fault tolerant, so if your app encounters an error and quits unexpectedly, you could lose the data.localStorage
only supports persisting strings. This module supports any JSON supported type.vuex-electron uses electron-store
to share your Vuex Store between all processes (including main).
~/Library/Caches/electron
electron
package is bundled as part of the built output. There is no need for your user to get electron
from npm to use your built app. Therefore it matches well the definition of a devDependency
. (When you publish your package, if the consumer project needs other packages to use yours, then these must be listed as dependencies
.) For example, VS Code properly lists electron
as a devDependency only: https://github.com/microsoft/vscode/blob/main/package.jsonhistory
mode. In Electron, it only works in hash
mode.
- 本地开发时是 http 服务,当访问某个地址的时候,其实真实目录下是没有这个文件的,本地服务可以帮助重定向到
/index.html
这是一定存在的入口文件,相当于走前端路由。一但打包之后,页面就是静态文件存放在目录中了,Electron 是找不到类似/index/page/1/2
这样的目录的,所以需要使用/index.html#page/1/2
这样的 hash 模式。同样,如果是 Web 项目使用了 history 模式打包,如果不在 nginx 中将全部 url 指向./index.html
的话,也会出现 404 的错误,也就是需要把路由移交给前端去控制。- hash mode 是默认模式,原理是使用
location.hash
和onhashchange
事件,利用#
后面的内容不会被发送到服务端实现单页应用。history mode 要手动设置mode: 'history'
, 是基于 History API 来实现的,这也是浏览器本身的功能,地址不会被请求到服务端。
/Applications/Demo.app/Contents/MacOS/
路径,执行 ./Demo
启动应用层序。Windows 上打开 Powershell 进入到程序的安装目录,执行 .\Demo.exe
,如果文件名中有空格,需要用双引号把文件名引起来。