标签 Web 下的文章

whistle 开源的请求调试工具,通过 NPM 安装,对前端程序员操作友好

它能做什么,常用功能?

可以通过设置网络代理, 调试所有设备上的网络请求

  • 抓包,可看到请求响应参数
  • 修改请求 or 响应,可以修改请求及响应的内容
  • 设置代理,可以将制定域名或者 ip 代理到本地机器调试
  • 移动端调试,内置了调试工具,可以将手机上网页的 DOM 结构,及 console 日志,输出在电脑上

whistle.png

安装及配置

安装 whistle 本体

npm install -g whistle
# 启动
w2 start

安装浏览器插件

安装 Whistle 第三方插件)

image-20200817162957076.png

配置移动端代理

无线局域网 > 选中使用的 WIFI 配置 > 配置代理 > 手动配置 > 填写你局域网 IP 及 w2 默认端口 8899

image-20200817163522590.png

配置HTTPS

上面配置已经可以抓到 HTTP 的包,对于HTTPS请求,需要安装 Whistle 提供的证书,否则会提示证书不安全

打开 Whistle 控制面板,点击顶部 HTTPS 二维码安装, 点这里 查看官方教程

注意:这一步很重要,关于本机 > 最底部证书信任设置 > 开启刚刚安装的证书

image-20200817164338083.png

常见规则配置

代理本地电脑 IP + 端口 到指定域名

192.168.13.73:3100 auth.juzisang.com

调试移动端指定域名 DOM 及 Console

# 在手机
auth2.txxy.com weinre://testDebug

替换响应内容

api.juzisang.com/account/status resBody://C:/Users/juzisang/Downloads/test.json

配置代理

注意,本地代理和 w2 是不能共存的,需要配置 w2 对应请求走代理

# 所有请求都走代理
/./ proxy://127.0.0.1:1081

# 根据 PAC 规则走代理
/./ pac://http://127.0.0.1:1082/pac/?103414

参考

介绍一下,开发一个 Lib,使用 rollup 的配置流程

文章所有代码地址: https://github.com/JuZiSang/coding-test/tree/master/Web/Rollup

安装所需依赖

# 安装 rollup
yarn add rollup -D

# 安装 babel 所需依赖
yarn add @babel/core @babel/plugin-syntax-dynamic-import @babel/plugin-transform-runtime @babel/preset-env @babel/runtime @rollup/plugin-babel -D
yarn add core-js

# 安装 typescript 所需依赖
yarn add rollup-plugin-typescript2 typescript -D

# 安装样式相关依赖
yarn add rollup-plugin-postcss sass -D

# 安装编译 nodejs 模块所需依赖
yarn add @rollup/plugin-node-resolve @rollup/plugin-commonjs -D

打包配置

项目结构

├── src
│   ├── lib
│   │   └── index.ts
│   └── main.ts
├── .browserslistrc
├── babel.config.js
├── package.json
├── rollup.config.js
├── tsconfig.json
└── yarn.lock

rollup.config.js

import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from 'rollup-plugin-typescript2'
import babel from '@rollup/plugin-babel'
import postcss from 'rollup-plugin-postcss'
import autoprefixer from 'autoprefixer'

export default {
  input: 'src/lib/index.ts',
  output: [
    { file: 'dist/lib.umd.js', name: 'Lib', format: 'umd' },
    { file: 'dist/lib.global.js', name: 'Lib', format: 'iife' },
    { file: 'dist/lib.esm.js', format: 'esm' }
  ],
  plugins: [
    // 可以 import node_modules 中的模块
    resolve(),
    // 将 commonjs 规范的模块,转换为 es6
    commonjs(),
    // typescript 转换,只有最基础的转换,将 ts > es6
    typescript(),
    // babel 编译 ts 转换出的 es6 模块
    babel({
      exclude: 'node_modules/**',
      babelHelpers: 'bundled',
      extensions: ['.js', '.ts']
    }),
    // 转换 scss
    postcss({
      extract: 'outline.css',
      extensions: ['scss', 'css'],
      minimize: true,
      plugins: [autoprefixer()]
    })
  ]
}

babel.config.js

module.exports = {
  presets: [
    ['@babel/preset-env', {
      'useBuiltIns': 'usage',
      'corejs': 3
    }]
  ],
  plugins: [
    '@babel/plugin-syntax-dynamic-import'
  ],
} 

tsconfig.json

配置基本只用来引导 vscode 提示,不做其他转换

{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "importHelpers": false,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "sourceMap": false,
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "src/*"
      ]
    },
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}
  • .browserslistrc

    postcss 看的

> 1%
last 2 versions
not dead

开发环境

配置一个改动 js 及 style 能自动重载的环境

# nollup 在 rollup 上实现 重载 or 热重载
# cross-env 用来设置环境变量
yarn add nollup cross-env -D

项目结构

├── src
│   ├── lib
│   │   └── index.ts
│   └── main.ts
├── examples # 新增
│   └── index.html
├── .browserslistrc
├── .nolluprc.js # 新增
├── babel.config.js
├── package.json
├── rollup.config.js
├── tsconfig.json
└── yarn.lock

.nolluprc.js

module.exports = {
  port: 8080,
  hot: true,
  contentBase: 'examples'
}

package.json

  "scripts": {
    "start": "cross-env NODE_ENV=development nollup --config rollup.config.js",
    "build": "cross-env NODE_ENV=production rollup --config rollup.config.js"
  }

rollup.config.js

import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from 'rollup-plugin-typescript2'
import babel from '@rollup/plugin-babel'
import postcss from 'rollup-plugin-postcss'
import autoprefixer from 'autoprefixer'
// 区分开发环境
const isDev = process.env.NODE_ENV === 'development'

export default {
  // 开发环境使用不同的入口
  input: isDev ? 'src/main.ts' : 'src/lib/index.ts',
  output: isDev
    ? [{ file: 'examples/main.js', sourcemap: true, format: 'iife' }]
    : [
      { file: 'dist/lib.umd.js', name: 'Lib', format: 'umd' },
      { file: 'dist/lib.global.js', name: 'Lib', format: 'iife' },
      { file: 'dist/lib.esm.js', format: 'esm' }
    ],
  plugins: [
    resolve(),
    commonjs(),
    typescript(),
    babel({
      exclude: 'node_modules/**',
      babelHelpers: 'bundled',
      extensions: ['.js', '.ts']
    }),
    postcss({
      extract: 'outline.css',
      extensions: ['scss', 'css'],
      minimize: true,
      plugins: [autoprefixer()]
    })
  ]
}

examples/index.html

注意引入 css js,名字就是配置中的 output 中的名字

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Examples</title>
  <link rel="stylesheet" type="text/css" href="./outline.css">
</head>

<body>
  <div id="app"></div>
  <script src="./main.js"></script>
</body>

</html>

CompositionAPI 项目实战总结

最近公司有个全新的详情,采用 Vue2 + CompositionAPI 编写完成,中间尝试集成过 TSX,总结遇到的坑及迷惑点

遇到的一些问题

  • 直接使用reactive的key,用来传递,值并不是响应式的,必须使用 computed包一层
  • 如果当前组件在 slot 中,ref将会失效,模板Ref ,官方暂时没找到解决方案 #296
  • 使用 TSX/JSX官方没有给出,组件实例怎么对外暴露方法
  • 需要具备一定的 Ts 类型编程能力,才能较好得封装 useXXX
  • 关于全局注册的组件,如 vue-router vuex 方法给出的使用方案 插件开发 依赖注入
  • 各种useXXX是依赖setup上下文的,只能放在setup里立即执行到,vue 才能收集到依赖
  • CompositionAPI 侵入性太强,由于所有值都需要包

直接使用reactive的key,给方法传递参数

如下,直接将 mobile 传递给 useSendCodeuseSendCode 拿到的参数值,将永远是空字符串,因为 mobile 并不是一个响应式对象

const formData = reactive({
  name: '',
  mobile: ''
})
// 将手机号传递给发送验证码的方法
const { sendCode, isSend } = useSendCode(formData.mobile)

正确写法是,必须要使用computed包一层

const formData = reactive({
  name: '',
  mobile: ''
})
// 正确写法,后续拿到 调用 sendCode 时,sendCode 内部使用 unref 解包,拿到对应值
const { sendCode, isSend } = useSendCode(computed(() => formData.mobile))

如果当前组件在 slot 中,ref将会失效

使用过程中遇到个很低级的Bug,使用ref来拿到组件实例时,如果当前组件是在 slot中,则 ref始终为null, 官方暂时没找到解决方案 #296

如下示例,在页面中,refHeader refElement 始终为null,但是打开调试控制台,能看到 $refs 下是有值的

// 父组件  
<div class="PageContainer" :onLoad="handleLoad">
  <slot v-if="loading"></slot>
</div>

// 当前页面
<PageContainer>
  <Header ref="refHeader"></Header>
  <div ref="refElement">
  </div>
</PageContainer>

官方提供的 CompositionAPI 中获取 ref 的方法

{
  setup(){
    const refHeader = ref<any>(null)
    const refElement = ref<any>(null)
    return {
      refHeader,
      refElement
    }
  }
}

解决办法

利用 getCurrentInstance 获取到当前vue实例,如果自带的 ref 无法获取到值,就取 $refs 上的

import { Ref, ref, getCurrentInstance, computed } from '@vue/composition-api'

export function useRef<T extends any> (key: string): Ref<T> {
  const _this = getCurrentInstance() as any
  const refComponent = ref<T>()
  return computed({
    get () {
      return refComponent.value || _this.$refs[key]
    },
    set (newValue) {
      refComponent.value = newValue
    }
  })
}

使用 TSX/JSX官方没有给出,组件实例怎么对外暴露方法

vue3中写TSX/JSX会占用setup的返回值,外部将无法通过 $ref调用到组件中的方法,如下

{
  setup(){
    let message = ref('Hello World')
    return () => {
      return <div>{message.value}</div>
    }
  }
}

各种useXXX是依赖setup上下文的

当我们自己封装 useXXX 时,内部 onMounted onUnmountedhooks,这些方法只能放在setup方法下,不能放在 setup 会异步执行的函数下。

如下

{
  setup(){
    // 正确√
    const refMenu = useRef('refMenu')
   
    // 错误 ×
    function handleLoad(){
      // handleLoad 会异步调用,不能放在这里
      const refMenu = useRef('refMenu')
      return getAjax('/xxx')
      .then(() => xxx)
    }
    
    return {
      refMenu,
      handleLoad
    }
   }
}

CompositionAPI 侵入性太强

虽然官网已经说了 引入-ref-的心智负担 ,但是由于我们的项目用到了大量过去项目积累下的业务库,如果有出现频繁的函数需要传递多个参数(4-5个),并且这个函数会在多个地方调用,就会看到一大片 unref

doPay({
  name:unref(name),
  url:unref(url),
  sex:unref(sex),
  ...
})
// 我们可能需要一个中间层,来封装旧的工具库,而不是 setup 中频繁 unref
function usePay({name,url,sex}){
  return {
    pay(){
      return doPay({
        name:unref(name),
        url:unref(url),
        sex:unref(sex)
      })
    }
  }
}

关于 useXXX 写法的思考

与传统 ClassApi 编写思路其实没差,由于需要手动对外暴露返回值和方法,相对于 ClassApi 可能会更加斟酌一下,这个值 or 方法是否值得暴露,而不是像 ClassApi 无脑对外暴露

一些代码组织方面的问题

在 vue2 时代,函数,数值,计算属性,放在那,vue 都帮我们规划好了,但是会出现同一片业务代码,出现在好几个地方,

CompositionAPI 虽然宣传说解决了这个问题,但是实际,我们写业务代码,各个变量以及函数之间实际是有关联的,一个变量可能会被多个地方使用,我们按照官方提供的这个图,将 useXXX 返回的业务代码就近,放在与它相关的业务代码下之上,但是可能会随着业务的迭代,它的返回值会被另一处业务所引用,你可能需要挪动 useXXX 所放置的位置,并且这个动作,将会频繁出现在你编写一个新项目的时候

什么是服务器渲染(SSR)?

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。

为什么需要服务端渲染?

在保障现代 Web 程序开发体验的基础之下,给予程序

  • 更好的 SEO
  • 加载更快,减少白屏时间

使用服务的渲染,以下几点需要知道

  • 依赖浏览器运行环境的库,只能在特定的生命周期钩子中运行,如mounted
  • 必须要依赖 Nodejs 环境
  • 相比静态文件,将会占用大量的 CPU 资源

服务的渲染 和 预渲染

如果你不需要将 Ajax 获取的数据,同步在后端转换为 Html 输出,用 Vue 只是写了一个完全静态的页面,则完全可以不使用 SSR 来渲染,采用预渲染才是比较明智的选择

webpack prerender-spa-plugin 插件,可以将 Vue 项目转换为 Html,你可以和以前一样,作为静态文件部署

编写需要同时运作在服务端及客户端的代码,应该注意

  • 运行在服务端中的 Vue 实例,将会禁用响应式对象
  • 组件生命周期

    • 服务端:只有 beforeCreatecreated 会在服务器端渲染 (SSR) 过程中被调用
    • 客户端:其它生命周期方法会在客户端执行
    • 避免在 beforeCreatecreated 中调用 setInterval , 由于在服务端运行时,永远不会调用到销毁的生命周期 beforeDestroy ,我们没有机会销毁这个 timer
  • 不要访问特定平台独有的 API,如:windowdocument
  • 自定义指令

    • 不要在 SSR 应用程序中编写操作 DOM 的指令,可以用父子组件代替

开始从零配置

Vue 实例转换 Html,模板插值

  • vue-server-renderer 包,用来将 Vue 实例转换为 HTML 的主要工具

server.js

const renderer = require("vue-server-renderer").createRenderer();
app.get("/demo1/*", (req, res) => {
  const vueApp = new Vue({
    data: {
      url: req.url,
    },
    template: `<div> 这是一个 SSR 站点 {{url}} </div>`,
  });
  renderer.renderToString(vueApp, (err, html) => {
    // 将会得到 <div> 这是一个 SSR 站点 /demo1/xxx </div>
    // 注意,并不会生成 Html 文档,需要手动设置 Html 模板
    res.end(html);
  });
});
  • 设置模板,及模板插值

server.js

const renderer = require("vue-server-renderer").createRenderer({
  template: require("fs").readFileSync(
    path.join(__dirname, "./template.html"),
    "utf-8"
  ),
});

// 需要插入模板当值的值
const content = {
  title: "这是一个 HTML 模板",
  meta: `
      // 模板中最好带上这个,否则会乱码
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      `,
};

app.get("/demo2/*", (req, res) => {
  const vueApp = new Vue({
    data: {
      url: req.url,
    },
    template: `<div> 这是一个 SSR 站点 {{url}} </div>`,
  });

  renderer.renderToString(vueApp, content, (err, html) => {
    res.end(html);
  });
});

template.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) -->
    {{{ meta }}}
    <!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) -->
    <title>{{ title }}</title>
  </head>
  <body>
    <!-- 组件生成的内容,将会插入这里 -->
    <!--vue-ssr-outlet-->
  </body>
</html>

webpack 打包 Vue 文件,及客户端服务端任何引用

  • 配合 webpack 打包 Vue 组件
    我们将有两个入口,服务的运行入口,客户端运行入口。

1.png

我将 webpack 配置分为三个

  • config

    • webpack.base.config.js
    • webpack.client.config.js
    • webpack.server.config.js
  • src

    • App.vue
    • app.js client server 公共代码
    • entry-client.js
    • entry-server.js

app.js

import Vue from "vue";
import App from "./App.vue";
/**
 * entry-client.js entry-server.js 都会引用这个文件创建 Vue 实例
 * 导出一个工厂函数,用于创建新的
 * 应用程序、router 和 store 实例
 */
export function createApp() {
  const app = new Vue({
    render: (h) => h(App),
  });
  return { app };
}

entry-server.js

/**
 * 服务端入口,我们会在 server.js 中引用
 * 返回一个方法的原因是,可能会有一些配置,需要外部传递
 */
import { createApp } from "./app";

export default (context) => {
  const { app } = createApp();
  return app;
};

entry-client.js

/**
 * 客户端入口,此时已经是渲染好的Html,我们只需要激活节点,让静态html拥有Vue的能力
 * 如点击交互,各种显示隐藏
 */
import { createApp } from "./app";

const { app } = createApp();

app.$mount("#app");

base 配置见 webpack.base.config.js 不详细讲了,我们主要注意client server两个入口的打包配置

webpack.server.config.js

module.exports = merge(base, {
  target: "node",
  entry: "./src/entry-server.js",
  // 因为 nodejs 中只能用 require,所以打包输出采用 commonjs 规范
  output: {
    filename: "server-bundle.js",
    libraryTarget: "commonjs2",
    // 注意,这里默认导出最好这样填,否则你引入 server 端运行时
    // 可能需要 require('server-bundle').default
    libraryExport: "default",
  },
});

webpack.client.config.js

module.exports = merge(base, {
  entry: "./src/entry-client.js",
  output: {
    filename: "client-bundle.js",
  },
});

server.js

// 引入服务器的打包入口
const createApp = require("./dist/server-bundle");
// 模板插值 XXX
const content = {};
// 创建Vue实例
renderer.renderToString(createApp(), content, (err, html) => {
  res.end(html);
});

引入路由 vue-router

router.js

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export function createRouter() {
  return new Router({
    mode: 'history',
    routes: [
      {
        path: '/home',
        component: () => import('./views/Home.vue')
      },
      {
        path: '/about',
        component: () => import('./views/About.vue')
      },
      // 如果不写这个,刷新会报错,并且如果直接访问根路径,会一直转圈圈
      {
        path: '*',
        redirect: '/home'
      }
    ]
  })
}

app.js

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'

/**
 * 导出一个工厂函数,用于创建新的
 * 应用程序、router 和 store 实例
 */
export function createApp() {
  // 创建 router 实例
  const router = createRouter()
  
  const app = new Vue({
    // 注入 router 到根 Vue 实例
    router,
    render: h => h(App)
  })

  return { app, router }
}

entry-server.js

export default context => {
  return new Promise((resolve, reject) => {
    const { app, router } = createApp()
    // 设置服务器端 router 的位置
    router.push(context.url)
    // 等到 router 将可能的异步组件和钩子函数解析完
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      // 匹配不到的路由,执行 reject 函数,并返回 404
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }
      // Promise 应该 resolve 应用程序实例,以便它可以渲染
      resolve(app)
    }, reject)
  })
}

entry-client.js

import { createApp } from './app'

const { app, router } = createApp()

router.onReady(() => {
  app.$mount('#app')
})

引入 vuex

store.js

// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import Axios from 'axios'

Vue.use(Vuex)

export function createStore() {
  return new Vuex.Store({
    state: {
      items: {}
    },
    actions: {
      fetchItem({ commit }, id) {
        // 请求数据见 https://github.com/JuZiSang/vue-ssr-study/blob/master/step4/server.js
        // `store.dispatch()` 会返回 Promise,
        // 以便我们能够知道数据在何时更新
        return Axios.get('http://127.0.0.1:8880/api/item')
          .then((item) => {
            console.log(item.data.data)
            commit('setItem', item.data.data)
          })
          .catch(err => {
            console.log('catch:', err)
            throw err
          })
      }
    },
    mutations: {
      setItem(state, item) {
        state.items = item
      }
    }
  })
}

app.js

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
import { sync } from 'vuex-router-sync'

/**
 * 导出一个工厂函数,用于创建新的
 * 应用程序、router 和 store 实例
 */
export function createApp() {
  // 创建 router store 实例
  const router = createRouter()
  const store = createStore()

  // 同步路由状态(route state)到 store
  sync(store, router)

  const app = new Vue({
    // 注入 router store 到根 Vue 实例
    router,
    store,
    render: h => h(App)
  })

  // 暴露 app, router 和 store。
  return { app, router, store }
}

entry-server.js

import { createApp } from './app'

export default context => {
  return new Promise((resolve, reject) => {
    const { app, router } = createApp()
    // 设置服务器端 router 的位置
    router.push(context.url)
    // 等到 router 将可能的异步组件和钩子函数解析完
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      // 匹配不到的路由,执行 reject 函数,并返回 404
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }
      // Promise 应该 resolve 应用程序实例,以便它可以渲染
      resolve(app)
    }, reject)
  })
}

entry-client.js

import { createApp } from './app'

const { app, router, store } = createApp()

// 将 SSR 注入的数据,填充到Vuex
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

router.onReady(() => {
  app.$mount('#app')
})

CSS 管理

CSS 管理

参考

分析 Webpack build 出的源码,有易于我们理解,js 模块加载机制,接下来我会以最基础的配置为例,分析 mode development 模式下的源码

源码地址:https://github.com/JuZiSang/coding-test/tree/master/tools/webpack/demo01

基础打包 JS

module.exports = {
  // 模式,默认两种, production development
  // production 生产环境,打包出来的 js 会进行,压缩混淆,优化
  // development 开发环境 不会压缩代码
  mode: "development",
  // 入口
  entry: "./src/main.js",
  // 出口
  output: {
    // 打包后的文件名,hash:8 名字中带 8位 hash
    filename: "bundle.[hash:8].js",
    // 打包路径,必须是绝对路径
    path: path.resolve("dist")
  }
};

webpack 打包文件分析

可以看到,模块只会安装一次,后续的引入只会返回第一次的结果的引用

// 立即执行函数,modules 就是我们引入的模块
(function(modules) {
  // webpackBootstrap
  // The module cache
  // 缓存,已经安装的模块缓存
  var installedModules = {};

  // The require function
  // 定义了一个 require 函数
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    // 缓存里有这个模块,直接返回
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    // 创建一个新模块,并添加到 缓存中
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    });

    // Execute the module function
    // 执行这个模块
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    // 模块执行完毕
    module.l = true;

    // Return the exports of the module
    // 返回模块执行结果
    return module.exports;
  }

  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules;

  // expose the module cache
  __webpack_require__.c = installedModules;

  // define getter function for harmony exports
  __webpack_require__.d = function(exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };

  // define __esModule on exports
  __webpack_require__.r = function(exports) {
    if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
    }
    Object.defineProperty(exports, "__esModule", { value: true });
  };

  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function(value, mode) {
    if (mode & 1) value = __webpack_require__(value);
    if (mode & 8) return value;
    if (mode & 4 && typeof value === "object" && value && value.__esModule) return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, "default", { enumerable: true, value: value });
    if (mode & 2 && typeof value != "string")
      for (var key in value)
        __webpack_require__.d(
          ns,
          key,
          function(key) {
            return value[key];
          }.bind(null, key)
        );
    return ns;
  };

  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function(module) {
    var getter =
      module && module.__esModule
        ? function getDefault() {
            return module["default"];
          }
        : function getModuleExports() {
            return module;
          };
    __webpack_require__.d(getter, "a", getter);
    return getter;
  };

  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function(object, property) {
    return Object.prototype.hasOwnProperty.call(object, property);
  };

  // __webpack_public_path__
  __webpack_require__.p = "";

  // Load entry module and return exports
  // 引入并执行入口模块
  return __webpack_require__((__webpack_require__.s = "./src/main.js"));
})({
  // 引入的 a 模块
  /*! no static exports found */
  "./src/a.js": function(module, exports) {
    eval('module.exports = "str";\n\n\n//# sourceURL=webpack:///./src/a.js?');
  },
  // 我们的入口
  "./src/main.js":
    /*! no static exports found */
    function(module, exports, __webpack_require__) {
      eval('const str = __webpack_require__(/*! ./a */ "./src/a.js");\n\nconsole.log(str);\n\n\n//# sourceURL=webpack:///./src/main.js?');
    }
});

CORS 全称 Cross-origin resource sharing(跨域资源共享)

用来解决浏览器跨域的其中一个方案

什么时候需要CORS

使用 Ajax 发起一个请求,如果这个请求的协议、域名、端口只要有一个与当前文档所处的地址不同,就会出现报错。

error1.png

而为了解决这个问题,就需要用到 CORS

具体可以阅读浏览器的同源策略

怎么使用CORS

只需要服务器在请求,响应头里增加

// 允许制定域名访问,注意协议、域名、端口要写全
Access-Control-Allow-Origin:https://www.juzisang,com

// 允许所有域名访问
Access-Control-Allow-Origin:*

请求分类

请求分为两种,简单请求、复杂请求,如果我们只设置了上面一个响应头,那么只允许发起简单请求

简单请求

  • 允许的方法:GETHEADPOST
  • 允许的请求头

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • 更多...
  • Content-Type

    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

复杂请求

使用了简单请求以外的 Http 方法或者请求头,浏览器将会发起一个 OPTIONS 预请求。

如果服务器没有返回对应的信息,将会报如下错误

  • 添加简单请求以外的请求头

error2.png

  • 使用简单请求以外的方法

error3.png

发起复杂请求

工作主要集中在服务端,需要服务端在响应中返回对应的响应头,允许我们添加这些属性,及使用这些方法

允许 Http 方法

// 允许指定方法,逗号隔开
"Access-Control-Allow-Methods": "PUT,DELETE,GET,POST"

// 允许所有方法
"Access-Control-Allow-Headers": "*"

允许自定义请求头

// 允许指定请求头,逗号隔开
"Access-Control-Allow-Headers": "XXX,Authorization"

允许传输 Cookie

// 设置为 true 允许
Access-Control-Allow-Credentials:true

OPTIONS 方法间隔时间

如果没有设置这个头,每次请求都会发起 OPTIONS

// 第二次请求,间隔直接在 1000 秒之内,不在发起 OPTIONS 请求
"Access-Control-Max-Age": 1000

允许客户端可以读取的响应头

// 允许客户端读取 XXX,CHeader
Access-Control-Expose-Headers:"XXX,CHeader"

// 允许所有响应头可被读取
Access-Control-Expose-Headers:"*"

代码

上面演示实例及报错实例在这里

Vue-Cli3 中虽然提供了 TypeScript 写 Vue,但是由于插件的支持度不够,Vue 文件里写 Ts,体验貌似也没什么提升,所以,我决定使用 TSX 来写 Vue

接下来,我以 Vue 初始项目为例,演示怎么将 Vue 文件转换成 Tsx

新建项目

我们使用 Vue-Cli3 新建一个项目,配置如下

vue-cli-options.png

vue-tsx-support 增强提示

yarn add vue-tsx-support -D

专门为了 Vue Tsx 文件,而编写的一个依赖,详细请阅读官网

或者参考我的项目里 https://github.com/JuZiSang/blog 的用法

Tsx 热重置

所需依赖

yarn add vue-jsx-hot-loader -D

vue.config.js

chainWebpack 增加如下 loader,支持热重置

config.module
  .rule("tsx")
  .test(/\.tsx$/)
  .use("vue-jsx-hot-loader")
  .before("babel-loader")
  .loader("vue-jsx-hot-loader");

CSSModule 支持

Tsx 中,Vue 原来的 scoped 方案就失效了,所以我决定使用 css-module 解决这个问题,并且配合 typings-for-css-modules-loader 生成 *.d.ts 增加提示

依赖

yarn add typings-for-css-modules-loader node-sass sass-loader -D

vue.config.js

["css", "less", "scss", "sass", "stylus", "postcss"].forEach(rule => {
  // rules for *.module.* files
  // 将 css-loader 替换为 typings-for-css-modules-loader
  const cssRule = config.module
    .rule(rule)
    .oneOf("normal-modules")
    .uses.get("css-loader")
    .set("loader", "typings-for-css-modules-loader");
});

我们可以继续在 vue.config.js 的 css 选项中配置我们的 css,详情戳这里

{
  loaderOptions: {
    css: {
      // name 导出
      namedExport: true,
      // 是否使用驼峰
      camelCase: true,
      // html 中 实际的 css 类名
      localIdentName: process.env.NODE_ENV !== "production" ? "[local]-[hash:base64:5]" : "[hash:base64:5]"
    }
  }
};

支持 sync v-model

依赖

yarn add babel-plugin-jsx-v-model babel-plugin-vue-jsx-sync -D

babel.config.js

module.exports = {
  presets: ["@vue/app"],
  plugins: ["vue-jsx-sync", "jsx-v-model"]
};

总结

yarn add vue-tsx-support vue-jsx-hot-loader typings-for-css-modules-loader node-sass sass-loader babel-plugin-jsx-v-model babel-plugin-vue-jsx-sync -D

经过如上改造,就能愉快的编写 Tsx 了

源码地址